summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
author Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> 2024-03-08 17:44:25 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-03-08 17:44:25 +0000
commit17e885ca0179f54e51ba6f5c07cf459868fe6c43 (patch)
tree4248d3408f200dc7596baf824d14732fc1dafa87 /java/src
parent339476e79a2dcff304206ba50f81c197cccac1b5 (diff)
parent8f8cf85bd1252bc62065b42afef104440fb4fe26 (diff)
Merge "Shareousel ui layer" into main
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java2
-rw-r--r--java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt3
-rw-r--r--java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt147
-rw-r--r--java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt18
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt372
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt51
-rw-r--r--java/src/com/android/intentresolver/contentpreview/SelectionTracker.kt175
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt107
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/app/viewmodel/ShareouselContentPreviewViewModel.kt46
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ComposeIconComposable.kt (renamed from java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ComposeIconComposable.kt)12
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt (renamed from java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselCardComposable.kt)8
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt (renamed from java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt)132
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ActionChipViewModel.kt (renamed from java/src/com/android/intentresolver/contentpreview/MutableActionFactory.kt)24
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt39
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt141
-rw-r--r--java/src/com/android/intentresolver/contentpreview/shareousel/ui/viewmodel/ShareouselViewModel.kt90
-rw-r--r--java/src/com/android/intentresolver/inject/Qualifiers.kt5
-rw-r--r--java/src/com/android/intentresolver/inject/ViewModelCoroutineScopeModule.kt42
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivity.java23
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserMutableActionFactory.kt54
20 files changed, 439 insertions, 1052 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 039fad56..e36e9df3 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -305,9 +305,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
.get(BasePreviewViewModel.class);
previewViewModel.init(
mChooserRequest.getTargetIntent(),
- getIntent(),
/*additionalContentUri = */ null,
- /*focusedItemIdx = */ 0,
/*isPayloadTogglingEnabled = */ false);
mChooserContentPreviewUi = new ChooserContentPreviewUi(
getCoroutineScope(getLifecycle()),
diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
index 21c909ea..dc36e584 100644
--- a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
@@ -25,14 +25,11 @@ import androidx.lifecycle.ViewModel
abstract class BasePreviewViewModel : ViewModel() {
@get:MainThread abstract val previewDataProvider: PreviewDataProvider
@get:MainThread abstract val imageLoader: ImageLoader
- abstract val payloadToggleInteractor: PayloadToggleInteractor?
@MainThread
abstract fun init(
targetIntent: Intent,
- chooserIntent: Intent,
additionalContentUri: Uri?,
- focusedItemIdx: Int,
isPayloadTogglingEnabled: Boolean,
)
}
diff --git a/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt b/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt
deleted file mode 100644
index 6a12f56c..00000000
--- a/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt
+++ /dev/null
@@ -1,147 +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.database.Cursor
-import android.database.MatrixCursor
-import android.net.Uri
-import android.os.Bundle
-import android.os.CancellationSignal
-import android.service.chooser.AdditionalContentContract.Columns
-import android.service.chooser.AdditionalContentContract.CursorExtraKeys
-import android.util.Log
-import android.util.SparseArray
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.coroutineScope
-
-private const val TAG = ContentPreviewUi.TAG
-
-/**
- * A bi-directional cursor reader. Reads URI from the [cursor] starting from the given [startPos],
- * filters items by [predicate].
- */
-class CursorUriReader(
- private val cursor: Cursor,
- startPos: Int,
- private val pageSize: Int,
- private val predicate: (Uri) -> Boolean,
-) : PayloadToggleInteractor.CursorReader {
- override val count = cursor.count
- // Unread ranges are:
- // - left: [0, leftPos);
- // - right: [rightPos, count)
- // i.e. read range is: [leftPos, rightPos)
- private var rightPos = startPos.coerceIn(0, count)
- private var leftPos = rightPos
-
- override val hasMoreBefore
- get() = leftPos > 0
-
- override val hasMoreAfter
- get() = rightPos < count
-
- override fun readPageAfter(): SparseArray<Uri> {
- if (!hasMoreAfter) return SparseArray()
- if (!cursor.moveToPosition(rightPos)) {
- rightPos = count
- Log.w(TAG, "Failed to move the cursor to position $rightPos, stop reading the cursor")
- return SparseArray()
- }
- val result = SparseArray<Uri>(pageSize)
- do {
- cursor
- .getString(0)
- ?.let(Uri::parse)
- ?.takeIf { predicate(it) }
- ?.let { uri -> result.append(rightPos, uri) }
- rightPos++
- } while (result.size() < pageSize && cursor.moveToNext())
- maybeCloseCursor()
- return result
- }
-
- override fun readPageBefore(): SparseArray<Uri> {
- if (!hasMoreBefore) return SparseArray()
- val startPos = maxOf(0, leftPos - pageSize)
- if (!cursor.moveToPosition(startPos)) {
- leftPos = 0
- Log.w(TAG, "Failed to move the cursor to position $startPos, stop reading cursor")
- return SparseArray()
- }
- val result = SparseArray<Uri>(leftPos - startPos)
- for (pos in startPos until leftPos) {
- cursor
- .getString(0)
- ?.let(Uri::parse)
- ?.takeIf { predicate(it) }
- ?.let { uri -> result.append(pos, uri) }
- if (!cursor.moveToNext()) break
- }
- leftPos = startPos
- maybeCloseCursor()
- return result
- }
-
- private fun maybeCloseCursor() {
- if (!hasMoreBefore && !hasMoreAfter) {
- close()
- }
- }
-
- override fun close() {
- cursor.close()
- }
-
- companion object {
- suspend fun createCursorReader(
- contentResolver: ContentInterface,
- uri: Uri,
- chooserIntent: Intent
- ): CursorUriReader {
- val cancellationSignal = CancellationSignal()
- val cursor =
- try {
- coroutineScope {
- runCatching {
- contentResolver.query(
- uri,
- arrayOf(Columns.URI),
- Bundle().apply {
- putParcelable(Intent.EXTRA_INTENT, chooserIntent)
- },
- cancellationSignal
- )
- }
- .getOrNull()
- ?: MatrixCursor(arrayOf(Columns.URI))
- }
- } catch (e: CancellationException) {
- cancellationSignal.cancel()
- throw e
- }
- return CursorUriReader(
- cursor,
- cursor.extras?.getInt(CursorExtraKeys.POSITION, 0) ?: 0,
- 128,
- ) {
- it.authority != uri.authority
- }
- }
- }
-}
diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
index 6e126822..e92d9bc6 100644
--- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
+++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
@@ -20,6 +20,12 @@ import android.content.Context
import android.util.PluralsMessageFormatter
import androidx.annotation.StringRes
import com.android.intentresolver.R
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
private const val PLURALS_COUNT = "count"
@@ -27,7 +33,11 @@ private const val PLURALS_COUNT = "count"
* HeadlineGenerator generates the text to show at the top of the sharesheet as a brief description
* of the content being shared.
*/
-class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {
+class HeadlineGeneratorImpl
+@Inject
+constructor(
+ @ApplicationContext private val context: Context,
+) : HeadlineGenerator {
override fun getTextHeadline(text: CharSequence): String {
return context.getString(
getTemplateResource(text, R.string.sharing_link, R.string.sharing_text)
@@ -100,3 +110,9 @@ class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {
return if (text.toString().isHttpUri()) linkResource else nonLinkResource
}
}
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface HeadlineGeneratorModule {
+ @Binds fun bind(impl: HeadlineGeneratorImpl): HeadlineGenerator
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt b/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt
deleted file mode 100644
index cc82c0a9..00000000
--- a/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt
+++ /dev/null
@@ -1,372 +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.Intent
-import android.net.Uri
-import android.service.chooser.ChooserAction
-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
-import java.util.concurrent.atomic.AtomicReference
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.channels.BufferOverflow.DROP_LATEST
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-
-private const val TAG = "PayloadToggleInteractor"
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class PayloadToggleInteractor(
- // TODO: a single-thread dispatcher is currently expected. iterate on the synchronization logic.
- private val scope: CoroutineScope,
- private val initiallySharedUris: List<Uri>,
- private val focusedUriIdx: Int,
- private val mimeTypeClassifier: MimeTypeClassifier,
- private val cursorReaderProvider: suspend () -> CursorReader,
- private val uriMetadataReader: (Uri) -> FileInfo,
- private val targetIntentModifier: (List<Item>) -> Intent,
- private val selectionCallback: suspend (Intent) -> ShareouselUpdate?,
-) {
- private var cursorDataRef = CompletableDeferred<CursorData?>()
- private val records = LinkedList<Record>()
- private val prevPageLoadingGate = AtomicBoolean(true)
- private val nextPageLoadingGate = AtomicBoolean(true)
- private val notifySelectionJobRef = AtomicReference<Job?>()
- private val emptyState =
- State(
- emptyList(),
- hasMoreItemsBefore = false,
- hasMoreItemsAfter = false,
- allowSelectionChange = false
- )
-
- private val stateFlowSource = MutableStateFlow(emptyState)
-
- val customActions =
- MutableSharedFlow<List<ChooserAction>>(replay = 1, onBufferOverflow = DROP_LATEST)
-
- val stateFlow: Flow<State>
- get() = stateFlowSource.filter { it !== emptyState }
-
- val targetPosition: Flow<Int> = stateFlow.map { it.targetPos }
- val previewKeys: Flow<List<Item>> = stateFlow.map { it.items }
-
- fun getKey(item: Any): Int = (item as Item).key
-
- fun selected(key: Item): Flow<Boolean> = (key as Record).isSelected
-
- fun previewUri(key: Item): Flow<Uri?> = flow { emit(key.previewUri) }
-
- fun previewInteractor(key: Any): PayloadTogglePreviewInteractor {
- val state = stateFlowSource.value
- if (state === emptyState) {
- Log.wtf(TAG, "Requesting item preview before any item has been published")
- } else {
- if (state.hasMoreItemsBefore && key === state.items.firstOrNull()) {
- loadMorePreviousItems()
- }
- if (state.hasMoreItemsAfter && key == state.items.lastOrNull()) {
- loadMoreNextItems()
- }
- }
- return PayloadTogglePreviewInteractor(key as Item, this)
- }
-
- init {
- scope
- .launch { awaitCancellation() }
- .invokeOnCompletion {
- cursorDataRef.cancel()
- runCatching {
- if (cursorDataRef.isCompleted && !cursorDataRef.isCancelled) {
- cursorDataRef.getCompleted()
- } else {
- null
- }
- }
- .getOrNull()
- ?.reader
- ?.close()
- }
- }
-
- fun start() {
- scope.launch {
- val cursorReader = cursorReaderProvider()
- val selectedItems =
- initiallySharedUris.map { uri ->
- val fileInfo = uriMetadataReader(uri)
- Record(
- 0, // artificial key for the pending record, it should not be used anywhere
- uri,
- fileInfo.previewUri,
- fileInfo.mimeType,
- )
- }
- val cursorData =
- CursorData(
- cursorReader,
- SelectionTracker(selectedItems, focusedUriIdx, cursorReader.count) { uri },
- )
- if (cursorDataRef.complete(cursorData)) {
- doLoadMorePreviousItems()
- val startPos = records.size
- doLoadMoreNextItems()
- prevPageLoadingGate.set(false)
- nextPageLoadingGate.set(false)
- publishSnapshot(startPos)
- } else {
- cursorReader.close()
- }
- }
- }
-
- fun loadMorePreviousItems() {
- invokeAsyncIfNotRunning(prevPageLoadingGate) {
- doLoadMorePreviousItems()
- publishSnapshot()
- }
- }
-
- fun loadMoreNextItems() {
- invokeAsyncIfNotRunning(nextPageLoadingGate) {
- doLoadMoreNextItems()
- publishSnapshot()
- }
- }
-
- fun setSelected(item: Item, isSelected: Boolean) {
- val record = item as Record
- scope.launch {
- val (_, selectionTracker) = waitForCursorData() ?: return@launch
- if (selectionTracker.setItemSelection(record.key, record, isSelected)) {
- val targetIntent = targetIntentModifier(selectionTracker.getSelection())
- val newJob = scope.launch { notifySelectionChanged(targetIntent) }
- notifySelectionJobRef.getAndSet(newJob)?.cancel()
- record.isSelected.value = selectionTracker.isItemSelected(record.key)
- }
- }
- }
-
- private fun invokeAsyncIfNotRunning(guardingFlag: AtomicBoolean, block: suspend () -> Unit) {
- if (guardingFlag.compareAndSet(false, true)) {
- scope.launch { block() }.invokeOnCompletion { guardingFlag.set(false) }
- }
- }
-
- private suspend fun doLoadMorePreviousItems() {
- val (reader, selectionTracker) = waitForCursorData() ?: return
- if (!reader.hasMoreBefore) return
-
- val newItems = reader.readPageBefore().toItems()
- selectionTracker.onStartItemsAdded(newItems)
- for (i in newItems.size() - 1 downTo 0) {
- records.add(
- 0,
- (newItems.valueAt(i) as Record).apply {
- isSelected.value = selectionTracker.isItemSelected(key)
- }
- )
- }
- if (!reader.hasMoreBefore && !reader.hasMoreAfter) {
- val pendingItems = selectionTracker.getPendingItems()
- val newRecords =
- pendingItems.foldIndexed(SparseArray<Item>()) { idx, acc, item ->
- assert(item is Record) { "Unexpected pending item type: ${item.javaClass}" }
- val rec = item as Record
- val key = idx - pendingItems.size
- acc.append(
- key,
- Record(
- key,
- rec.uri,
- rec.previewUri,
- rec.mimeType,
- rec.mimeType?.mimeTypeToItemType() ?: ItemType.File
- )
- )
- acc
- }
-
- selectionTracker.onStartItemsAdded(newRecords)
- for (i in (newRecords.size() - 1) downTo 0) {
- records.add(0, (newRecords.valueAt(i) as Record).apply { isSelected.value = true })
- }
- }
- }
-
- private suspend fun doLoadMoreNextItems() {
- val (reader, selectionTracker) = waitForCursorData() ?: return
- if (!reader.hasMoreAfter) return
-
- val newItems = reader.readPageAfter().toItems()
- selectionTracker.onEndItemsAdded(newItems)
- for (i in 0 until newItems.size()) {
- val key = newItems.keyAt(i)
- records.add(
- (newItems.valueAt(i) as Record).apply {
- isSelected.value = selectionTracker.isItemSelected(key)
- }
- )
- }
- if (!reader.hasMoreBefore && !reader.hasMoreAfter) {
- val items =
- selectionTracker.getPendingItems().let { items ->
- items.foldIndexed(SparseArray<Item>(items.size)) { i, acc, item ->
- val key = reader.count + i
- val record = item as Record
- acc.append(
- key,
- Record(key, record.uri, record.previewUri, record.mimeType, record.type)
- )
- acc
- }
- }
- selectionTracker.onEndItemsAdded(items)
- for (i in 0 until items.size()) {
- records.add((items.valueAt(i) as Record).apply { isSelected.value = true })
- }
- }
- }
-
- private fun SparseArray<Uri>.toItems(): SparseArray<Item> {
- val items = SparseArray<Item>(size())
- for (i in 0 until size()) {
- val key = keyAt(i)
- val uri = valueAt(i)
- val fileInfo = uriMetadataReader(uri)
- items.append(
- key,
- Record(
- key,
- uri,
- fileInfo.previewUri,
- fileInfo.mimeType,
- fileInfo.mimeType?.mimeTypeToItemType() ?: ItemType.File
- )
- )
- }
- return items
- }
-
- private suspend fun waitForCursorData() = cursorDataRef.await()
-
- private suspend fun notifySelectionChanged(targetIntent: Intent) {
- selectionCallback(targetIntent)?.customActions?.let { customActions.tryEmit(it) }
- }
-
- private suspend fun publishSnapshot(startPos: Int = -1) {
- val (reader, _) = waitForCursorData() ?: return
- // TODO: publish a view into the list as it can only grow on each side thus a view won't be
- // invalidated
- val items = ArrayList<Item>(records)
- stateFlowSource.emit(
- State(
- items,
- reader.hasMoreBefore,
- reader.hasMoreAfter,
- allowSelectionChange = true,
- targetPos = startPos,
- )
- )
- }
-
- private fun String.mimeTypeToItemType(): ItemType =
- when {
- mimeTypeClassifier.isImageType(this) -> ItemType.Image
- mimeTypeClassifier.isVideoType(this) -> ItemType.Video
- else -> ItemType.File
- }
-
- class State(
- val items: List<Item>,
- val hasMoreItemsBefore: Boolean,
- val hasMoreItemsAfter: Boolean,
- val allowSelectionChange: Boolean,
- val targetPos: Int = -1,
- )
-
- sealed interface Item {
- val key: Int
- val uri: Uri
- val previewUri: Uri?
- val mimeType: String?
- val type: ItemType
- }
-
- enum class ItemType {
- Image,
- Video,
- File,
- }
-
- private class Record(
- override val key: Int,
- override val uri: Uri,
- override val previewUri: Uri? = uri,
- override val mimeType: String?,
- override val type: ItemType = ItemType.Image,
- ) : Item {
- val isSelected = MutableStateFlow(false)
- }
-
- private data class CursorData(
- val reader: CursorReader,
- val selectionTracker: SelectionTracker<Item>,
- )
-
- interface CursorReader : Closeable {
- val count: Int
- val hasMoreBefore: Boolean
- val hasMoreAfter: Boolean
-
- fun readPageAfter(): SparseArray<Uri>
-
- fun readPageBefore(): SparseArray<Uri>
- }
-}
-
-class PayloadTogglePreviewInteractor(
- private val item: PayloadToggleInteractor.Item,
- private val interactor: PayloadToggleInteractor,
-) {
- fun setSelected(selected: Boolean) {
- interactor.setSelected(item, selected)
- }
-
- val previewUri: Flow<Uri?>
- get() = interactor.previewUri(item)
-
- val selected: Flow<Boolean>
- get() = interactor.selected(item)
-
- val key
- get() = item.key
-}
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
index f79f0525..6a729945 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
@@ -27,13 +27,9 @@ 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
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.plus
/** A view model for the preview logic */
@@ -44,9 +40,7 @@ class PreviewViewModel(
@Background private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : BasePreviewViewModel() {
private var targetIntent: Intent? = null
- private var chooserIntent: Intent? = null
private var additionalContentUri: Uri? = null
- private var focusedItemIdx: Int = 0
private var isPayloadTogglingEnabled = false
override val previewDataProvider by lazy {
@@ -69,64 +63,19 @@ class PreviewViewModel(
)
}
- override val payloadToggleInteractor: PayloadToggleInteractor? by lazy {
- val targetIntent = requireNotNull(targetIntent) { "Not initialized" }
- // TODO: replace with flags injection
- if (!isPayloadTogglingEnabled) return@lazy null
- createPayloadToggleInteractor(
- additionalContentUri ?: return@lazy null,
- targetIntent,
- chooserIntent ?: return@lazy null,
- )
- .apply { start() }
- }
-
// TODO: make the view model injectable and inject these dependencies instead
@MainThread
override fun init(
targetIntent: Intent,
- chooserIntent: Intent,
additionalContentUri: Uri?,
- focusedItemIdx: Int,
isPayloadTogglingEnabled: Boolean,
) {
if (this.targetIntent != null) return
this.targetIntent = targetIntent
- this.chooserIntent = chooserIntent
this.additionalContentUri = additionalContentUri
- this.focusedItemIdx = focusedItemIdx
this.isPayloadTogglingEnabled = isPayloadTogglingEnabled
}
- private fun createPayloadToggleInteractor(
- contentProviderUri: Uri,
- targetIntent: Intent,
- chooserIntent: Intent,
- ): PayloadToggleInteractor {
- return PayloadToggleInteractor(
- // TODO: update PayloadToggleInteractor to support multiple threads
- viewModelScope + Executors.newSingleThreadScheduledExecutor().asCoroutineDispatcher(),
- previewDataProvider.uris,
- maxOf(0, minOf(focusedItemIdx, previewDataProvider.uriCount - 1)),
- DefaultMimeTypeClassifier,
- {
- CursorUriReader.createCursorReader(
- contentResolver,
- contentProviderUri,
- chooserIntent
- )
- },
- UriMetadataReaderImpl(contentResolver, DefaultMimeTypeClassifier)::getMetadata,
- TargetIntentModifierImpl<PayloadToggleInteractor.Item>(
- targetIntent,
- getUri = { uri },
- getMimeType = { mimeType },
- )::onSelectionChanged,
- SelectionChangeCallbackImpl(contentProviderUri, chooserIntent, contentResolver)::
- onSelectionChanged,
- )
- }
-
companion object {
val Factory: ViewModelProvider.Factory =
object : ViewModelProvider.Factory {
diff --git a/java/src/com/android/intentresolver/contentpreview/SelectionTracker.kt b/java/src/com/android/intentresolver/contentpreview/SelectionTracker.kt
deleted file mode 100644
index c9431731..00000000
--- a/java/src/com/android/intentresolver/contentpreview/SelectionTracker.kt
+++ /dev/null
@@ -1,175 +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.net.Uri
-import android.util.SparseArray
-import android.util.SparseIntArray
-import androidx.core.util.containsKey
-import androidx.core.util.isNotEmpty
-
-/**
- * Tracks selected items (including those that has not been read frm the cursor) and their relative
- * order.
- */
-class SelectionTracker<Item>(
- selectedItems: List<Item>,
- private val focusedItemIdx: Int,
- private val cursorCount: Int,
- private val getUri: Item.() -> Uri,
-) {
- /** Contains selected items keys. */
- private val selections = SparseArray<Item>(selectedItems.size)
-
- /**
- * A set of initially selected items that has not yet been observed by the lazy read of the
- * cursor and thus has unknown key (cursor position). Initially, all [selectedItems] are put in
- * this map with items at the index less than [focusedItemIdx] with negative keys (to the left
- * of all cursor items) and items at the index more or equal to [focusedItemIdx] with keys more
- * or equal to [cursorCount] (to the right of all cursor items) in their relative order. Upon
- * reading the cursor, [onEndItemsAdded]/[onStartItemsAdded], all pending items from that
- * collection in the corresponding direction get their key assigned and gets removed from the
- * map. Items that were missing from the cursor get removed from the map by
- * [getPendingItems] + [onStartItemsAdded]/[onEndItemsAdded] combination.
- */
- private val pendingKeys = HashMap<Uri, SparseIntArray>()
-
- init {
- selectedItems.forEachIndexed { i, item ->
- // all items before focusedItemIdx gets "positioned" before all the cursor items
- // and all the reset after all the cursor items in their relative order.
- // Also see the comments to pendingKeys property.
- val key =
- if (i < focusedItemIdx) {
- i - focusedItemIdx
- } else {
- i + cursorCount - focusedItemIdx
- }
- selections.append(key, item)
- pendingKeys.getOrPut(item.getUri()) { SparseIntArray(1) }.append(key, key)
- }
- }
-
- /** Update selections based on the set of items read from the end of the cursor */
- fun onEndItemsAdded(items: SparseArray<Item>) {
- for (i in 0 until items.size()) {
- val item = items.valueAt(i)
- pendingKeys[item.getUri()]
- // if only one pending (unmatched) item with this URI is left, removed this URI
- ?.also {
- if (it.size() <= 1) {
- pendingKeys.remove(item.getUri())
- }
- }
- // a safeguard, we should not observe empty arrays at this point
- ?.takeIf { it.isNotEmpty() }
- // pick a matching pending items from the right side
- ?.let { pendingUriPositions ->
- val key = items.keyAt(i)
- val insertPos =
- pendingUriPositions
- .findBestKeyPosition(key)
- .coerceIn(0, pendingUriPositions.size() - 1)
- // select next pending item from the right, if not such item exists then
- // the data is inconsistent and we pick the closes one from the left
- val keyPlaceholder = pendingUriPositions.keyAt(insertPos)
- pendingUriPositions.removeAt(insertPos)
- selections.remove(keyPlaceholder)
- selections[key] = item
- }
- }
- }
-
- /** Update selections based on the set of items read from the head of the cursor */
- fun onStartItemsAdded(items: SparseArray<Item>) {
- for (i in (items.size() - 1) downTo 0) {
- val item = items.valueAt(i)
- pendingKeys[item.getUri()]
- // if only one pending (unmatched) item with this URI is left, removed this URI
- ?.also {
- if (it.size() <= 1) {
- pendingKeys.remove(item.getUri())
- }
- }
- // a safeguard, we should not observe empty arrays at this point
- ?.takeIf { it.isNotEmpty() }
- // pick a matching pending items from the left side
- ?.let { pendingUriPositions ->
- val key = items.keyAt(i)
- val insertPos =
- pendingUriPositions
- .findBestKeyPosition(key)
- .coerceIn(1, pendingUriPositions.size())
- // select next pending item from the left, if not such item exists then
- // the data is inconsistent and we pick the closes one from the right
- val keyPlaceholder = pendingUriPositions.keyAt(insertPos - 1)
- pendingUriPositions.removeAt(insertPos - 1)
- selections.remove(keyPlaceholder)
- selections[key] = item
- }
- }
- }
-
- /** Updated selection status for the given item */
- fun setItemSelection(key: Int, item: Item, isSelected: Boolean): Boolean {
- val idx = selections.indexOfKey(key)
- if (isSelected && idx < 0) {
- selections[key] = item
- return true
- }
- if (!isSelected && idx >= 0 && selections.size() > 1) {
- selections.removeAt(idx)
- return true
- }
- return false
- }
-
- /** Return selection status for the given item */
- fun isItemSelected(key: Int): Boolean = selections.containsKey(key)
-
- fun getSelection(): List<Item> =
- buildList(selections.size()) {
- for (i in 0 until selections.size()) {
- add(selections.valueAt(i))
- }
- }
-
- /** Return all selected items that has not yet been read from the cursor */
- fun getPendingItems(): List<Item> =
- if (pendingKeys.isEmpty()) {
- emptyList()
- } else {
- buildList {
- for (i in 0 until selections.size()) {
- val item = selections.valueAt(i) ?: continue
- if (isPending(item, selections.keyAt(i))) {
- add(item)
- }
- }
- }
- }
-
- private fun isPending(item: Item, key: Int): Boolean {
- val keys = pendingKeys[item.getUri()] ?: return false
- return keys.containsKey(key)
- }
-
- private fun SparseIntArray.findBestKeyPosition(key: Int): Int =
- // undocumented, but indexOfKey behaves in the same was as
- // java.util.Collections#binarySearch()
- indexOfKey(key).let { if (it < 0) it.inv() else it }
-}
diff --git a/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt
index 82c09986..80f7c25a 100644
--- a/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt
+++ b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt
@@ -22,27 +22,18 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.dimensionResource
-import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.intentresolver.R
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory
-import com.android.intentresolver.contentpreview.shareousel.ui.composable.Shareousel
-import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.ShareouselViewModel
-import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.toShareouselViewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.app.viewmodel.ShareouselContentPreviewViewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.composable.Shareousel
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
class ShareouselContentPreviewUi(
@@ -56,76 +47,48 @@ class ShareouselContentPreviewUi(
layoutInflater: LayoutInflater,
parent: ViewGroup,
headlineViewParent: View?,
- ): ViewGroup {
- return displayInternal(parent, headlineViewParent).also { layout ->
+ ): ViewGroup =
+ displayInternal(parent, headlineViewParent).also { layout ->
displayModifyShareAction(headlineViewParent ?: layout, actionFactory)
}
- }
- private fun displayInternal(
- parent: ViewGroup,
- headlineViewParent: View?,
- ): ViewGroup {
+ private fun displayInternal(parent: ViewGroup, headlineViewParent: View?): ViewGroup {
if (headlineViewParent != null) {
inflateHeadline(headlineViewParent)
}
- val composeView =
- ComposeView(parent.context).apply {
- setContent {
- val vm: BasePreviewViewModel = viewModel()
- val interactor =
- requireNotNull(vm.payloadToggleInteractor) { "Should not be null" }
+ return ComposeView(parent.context).apply {
+ setContent {
+ val vm: ShareouselContentPreviewViewModel = viewModel()
+ val viewModel: ShareouselViewModel = vm.viewModel
- var viewModel by remember { mutableStateOf<ShareouselViewModel?>(null) }
- LaunchedEffect(Unit) {
- viewModel =
- interactor.toShareouselViewModel(
- vm.imageLoader,
- actionFactory,
- vm.viewModelScope
- )
- }
+ headlineViewParent?.let {
+ LaunchedEffect(viewModel) { bindHeadline(viewModel, headlineViewParent) }
+ }
- headlineViewParent?.let {
- viewModel?.let { viewModel ->
- LaunchedEffect(viewModel) {
- viewModel.headline.collect { headline ->
- headlineViewParent
- .findViewById<TextView>(R.id.headline)
- ?.apply {
- if (headline.isNotBlank()) {
- text = headline
- visibility = View.VISIBLE
- } else {
- visibility = View.GONE
- }
- }
- }
- }
- }
- }
+ MaterialTheme(
+ colorScheme =
+ if (isSystemInDarkTheme()) {
+ dynamicDarkColorScheme(LocalContext.current)
+ } else {
+ dynamicLightColorScheme(LocalContext.current)
+ },
+ ) {
+ Shareousel(viewModel)
+ }
+ }
+ }
+ }
- viewModel?.let { viewModel ->
- MaterialTheme(
- colorScheme =
- if (isSystemInDarkTheme()) {
- dynamicDarkColorScheme(LocalContext.current)
- } else {
- dynamicLightColorScheme(LocalContext.current)
- },
- ) {
- Shareousel(viewModel = viewModel)
- }
- }
- ?: run {
- Spacer(
- Modifier.height(
- dimensionResource(R.dimen.chooser_preview_image_height_tall)
- )
- )
- }
+ private suspend fun bindHeadline(viewModel: ShareouselViewModel, headlineViewParent: View) {
+ viewModel.headline.collect { headline ->
+ headlineViewParent.findViewById<TextView>(R.id.headline)?.apply {
+ if (headline.isNotBlank()) {
+ text = headline
+ visibility = View.VISIBLE
+ } else {
+ visibility = View.GONE
}
}
- return composeView
+ }
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/app/viewmodel/ShareouselContentPreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/app/viewmodel/ShareouselContentPreviewViewModel.kt
new file mode 100644
index 00000000..479f0ec8
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/app/viewmodel/ShareouselContentPreviewViewModel.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.app.viewmodel
+
+import androidx.lifecycle.ViewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.FetchPreviewsInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.UpdateTargetIntentInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.inject.ViewModelOwned
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** View-model for [com.android.intentresolver.contentpreview.ShareouselContentPreviewUi]. */
+@HiltViewModel
+class ShareouselContentPreviewViewModel
+@Inject
+constructor(
+ val viewModel: ShareouselViewModel,
+ updateTargetIntentInteractor: UpdateTargetIntentInteractor,
+ fetchPreviewsInteractor: FetchPreviewsInteractor,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @ViewModelOwned private val scope: CoroutineScope,
+) : ViewModel() {
+ init {
+ scope.launch(bgDispatcher) { updateTargetIntentInteractor.launch() }
+ scope.launch(bgDispatcher) { fetchPreviewsInteractor.launch() }
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ComposeIconComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ComposeIconComposable.kt
index 87fb7618..38138225 100644
--- a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ComposeIconComposable.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ComposeIconComposable.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.contentpreview.shareousel.ui.composable
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.composable
import android.content.Context
import android.content.ContextWrapper
@@ -21,6 +21,7 @@ import android.content.res.Resources
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -30,10 +31,11 @@ import com.android.intentresolver.icon.ComposeIcon
import com.android.intentresolver.icon.ResourceIcon
@Composable
-fun Image(icon: ComposeIcon) {
+fun Image(icon: ComposeIcon, modifier: Modifier = Modifier) {
when (icon) {
- is AdaptiveIcon -> Image(icon.wrapped)
- is BitmapIcon -> Image(icon.bitmap.asImageBitmap(), contentDescription = null)
+ is AdaptiveIcon -> Image(icon.wrapped, modifier)
+ is BitmapIcon ->
+ Image(icon.bitmap.asImageBitmap(), contentDescription = null, modifier = modifier)
is ResourceIcon -> {
val localContext = LocalContext.current
val wrappedContext: Context =
@@ -41,7 +43,7 @@ fun Image(icon: ComposeIcon) {
override fun getResources(): Resources = icon.res
}
CompositionLocalProvider(LocalContext provides wrappedContext) {
- Image(painterResource(icon.resId), contentDescription = null)
+ Image(painterResource(icon.resId), contentDescription = null, modifier = modifier)
}
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselCardComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt
index dc96e3c1..f33558c7 100644
--- a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselCardComposable.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.contentpreview.shareousel.ui.composable
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -33,10 +33,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.android.intentresolver.R
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ContentType
@Composable
fun ShareouselCard(
image: @Composable () -> Unit,
+ contentType: ContentType,
selected: Boolean,
modifier: Modifier = Modifier,
) {
@@ -45,7 +47,9 @@ fun ShareouselCard(
val topButtonPadding = 12.dp
Box(modifier = Modifier.padding(topButtonPadding).matchParentSize()) {
SelectionIcon(selected, modifier = Modifier.align(Alignment.TopStart))
- AnimationIcon(modifier = Modifier.align(Alignment.TopEnd))
+ if (contentType == ContentType.Video) {
+ AnimationIcon(modifier = Modifier.align(Alignment.TopEnd))
+ }
}
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt
index 0b3cdd83..feb6f3a8 100644
--- a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.contentpreview.shareousel.ui.composable
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -24,10 +24,14 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AssistChip
@@ -35,65 +39,78 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.intentresolver.R
-import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.ShareouselImageViewModel
-import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.ShareouselViewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ContentType
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselPreviewViewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel
+import kotlinx.coroutines.launch
@Composable
fun Shareousel(viewModel: ShareouselViewModel) {
- val centerIdx = viewModel.centerIndex.value
- val carouselState = rememberLazyListState(initialFirstVisibleItemIndex = centerIdx)
- val previewKeys by viewModel.previewKeys.collectAsStateWithLifecycle()
- Column(modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer)) {
- // TODO: item needs to be centered, check out ScalingLazyColumn impl or see if
- // HorizontalPager works for our use-case
- LazyRow(
- state = carouselState,
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- modifier =
- Modifier.fillMaxWidth()
- .height(dimensionResource(R.dimen.chooser_preview_image_height_tall))
- ) {
- items(previewKeys, key = viewModel.previewRowKey) { key ->
- ShareouselCard(viewModel.previewForKey(key))
- }
- }
- Spacer(modifier = Modifier.height(8.dp))
+ val keySet = viewModel.previews.collectAsStateWithLifecycle(null).value
+ if (keySet != null) {
+ Shareousel(viewModel, keySet)
+ } else {
+ Spacer(
+ Modifier.height(dimensionResource(R.dimen.chooser_preview_image_height_tall) + 64.dp)
+ )
+ }
+}
- val actions by viewModel.actions.collectAsStateWithLifecycle(initialValue = emptyList())
- LazyRow(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- ) {
- items(actions) { actionViewModel ->
- ShareouselAction(
- label = actionViewModel.label,
- onClick = actionViewModel.onClick,
- ) {
- actionViewModel.icon?.let { Image(it) }
- }
- }
- }
+@Composable
+private fun Shareousel(viewModel: ShareouselViewModel, keySet: PreviewsModel) {
+ Column(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.surfaceContainer)
+ .padding(vertical = 16.dp),
+ ) {
+ PreviewCarousel(keySet, viewModel)
+ Spacer(Modifier.height(16.dp))
+ ActionCarousel(viewModel)
}
}
-private const val MIN_ASPECT_RATIO = 0.4f
-private const val MAX_ASPECT_RATIO = 2.5f
+@Composable
+private fun PreviewCarousel(
+ previews: PreviewsModel,
+ viewModel: ShareouselViewModel,
+) {
+ val centerIdx = previews.startIdx
+ val carouselState = rememberLazyListState(initialFirstVisibleItemIndex = centerIdx)
+ // TODO: start item needs to be centered, check out ScalingLazyColumn impl or see if
+ // HorizontalPager works for our use-case
+ LazyRow(
+ state = carouselState,
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ modifier =
+ Modifier.fillMaxWidth()
+ .height(dimensionResource(R.dimen.chooser_preview_image_height_tall))
+ ) {
+ items(previews.previewModels.toList(), key = { it.uri }) { model ->
+ ShareouselCard(viewModel.preview(model))
+ }
+ }
+}
@Composable
-private fun ShareouselCard(viewModel: ShareouselImageViewModel) {
+private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) {
val bitmap by viewModel.bitmap.collectAsStateWithLifecycle(initialValue = null)
val selected by viewModel.isSelected.collectAsStateWithLifecycle(initialValue = false)
- val contentDescription by
- viewModel.contentDescription.collectAsStateWithLifecycle(initialValue = null)
+ val contentType by
+ viewModel.contentType.collectAsStateWithLifecycle(initialValue = ContentType.Image)
val borderColor = MaterialTheme.colorScheme.primary
-
+ val scope = rememberCoroutineScope()
ShareouselCard(
image = {
bitmap?.let { bitmap ->
@@ -103,31 +120,55 @@ private fun ShareouselCard(viewModel: ShareouselImageViewModel) {
.coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO)
Image(
bitmap = bitmap.asImageBitmap(),
- contentDescription = contentDescription,
+ contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.aspectRatio(aspectRatio),
)
}
?: run {
// TODO: look at ScrollableImagePreviewView.setLoading()
- Box(modifier = Modifier.aspectRatio(2f / 5f))
+ Box(
+ modifier =
+ Modifier.fillMaxHeight()
+ .aspectRatio(2f / 5f)
+ .border(1.dp, Color.Red, RectangleShape)
+ )
}
},
+ contentType = contentType,
selected = selected,
modifier =
Modifier.thenIf(selected) {
Modifier.border(
width = 4.dp,
color = borderColor,
- shape = RoundedCornerShape(size = 12.dp)
+ shape = RoundedCornerShape(size = 12.dp),
)
}
.clip(RoundedCornerShape(size = 12.dp))
- .clickable { viewModel.setSelected(!selected) },
+ .clickable { scope.launch { viewModel.setSelected(!selected) } },
)
}
@Composable
+private fun ActionCarousel(viewModel: ShareouselViewModel) {
+ val actions by viewModel.actions.collectAsStateWithLifecycle(initialValue = emptyList())
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = Modifier.height(32.dp),
+ ) {
+ itemsIndexed(actions) { idx, actionViewModel ->
+ ShareouselAction(
+ label = actionViewModel.label,
+ onClick = { actionViewModel.onClicked() },
+ ) {
+ actionViewModel.icon?.let { Image(icon = it, modifier = Modifier.size(16.dp)) }
+ }
+ }
+ }
+}
+
+@Composable
private fun ShareouselAction(
label: String,
onClick: () -> Unit,
@@ -144,3 +185,6 @@ private fun ShareouselAction(
inline fun Modifier.thenIf(condition: Boolean, crossinline factory: () -> Modifier): Modifier =
if (condition) this.then(factory()) else this
+
+private const val MIN_ASPECT_RATIO = 0.4f
+private const val MAX_ASPECT_RATIO = 2.5f
diff --git a/java/src/com/android/intentresolver/contentpreview/MutableActionFactory.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ActionChipViewModel.kt
index 1cc1a6a6..728c573b 100644
--- a/java/src/com/android/intentresolver/contentpreview/MutableActionFactory.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ActionChipViewModel.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,16 +14,16 @@
* limitations under the License.
*/
-package com.android.intentresolver.contentpreview
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel
-import android.service.chooser.ChooserAction
-import com.android.intentresolver.widget.ActionRow
-import kotlinx.coroutines.flow.Flow
+import com.android.intentresolver.icon.ComposeIcon
-interface MutableActionFactory {
- /** A flow of custom actions */
- val customActionsFlow: Flow<List<ActionRow.Action>>
-
- /** Update custom actions */
- fun updateCustomActions(actions: List<ChooserAction>)
-}
+/** An action chip presented to the user underneath Shareousel. */
+data class ActionChipViewModel(
+ /** Text label. */
+ val label: String,
+ /** Optional icon, displayed next to the text label. */
+ val icon: ComposeIcon?,
+ /** Handles user clicks on this action in the UI. */
+ val onClicked: () -> Unit,
+)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt
new file mode 100644
index 00000000..a245b3e3
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.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.contentpreview.payloadtoggle.ui.viewmodel
+
+import android.graphics.Bitmap
+import kotlinx.coroutines.flow.Flow
+
+/** An individual preview within Shareousel. */
+data class ShareouselPreviewViewModel(
+ /** Image to be shared. */
+ val bitmap: Flow<Bitmap?>,
+ /** Type of data to be shared. */
+ val contentType: Flow<ContentType>,
+ /** Whether this preview has been selected by the user. */
+ val isSelected: Flow<Boolean>,
+ /** Sets whether this preview has been selected by the user. */
+ val setSelected: suspend (Boolean) -> Unit,
+)
+
+/** Type of the content being previewed. */
+enum class ContentType {
+ Image,
+ Video,
+ Other
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt
new file mode 100644
index 00000000..6eccaffa
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.ui.viewmodel
+
+import android.content.Context
+import com.android.intentresolver.R
+import com.android.intentresolver.contentpreview.HeadlineGenerator
+import com.android.intentresolver.contentpreview.ImageLoader
+import com.android.intentresolver.contentpreview.ImagePreviewImageLoader
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggle
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.CustomActionsInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectablePreviewsInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectionInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.inject.ViewModelOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.plus
+
+/** A dynamic carousel of selectable previews within share sheet. */
+data class ShareouselViewModel(
+ /** Text displayed at the top of the share sheet when Shareousel is present. */
+ val headline: Flow<String>,
+ /**
+ * Previews which are available for presentation within Shareousel. Use [preview] to create a
+ * [ShareouselPreviewViewModel] for a given [PreviewModel].
+ */
+ val previews: Flow<PreviewsModel?>,
+ /** List of action chips presented underneath Shareousel. */
+ val actions: Flow<List<ActionChipViewModel>>,
+ /** Creates a [ShareouselPreviewViewModel] for a [PreviewModel] present in [previews]. */
+ val preview: (key: PreviewModel) -> ShareouselPreviewViewModel,
+)
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object ShareouselViewModelModule {
+ @Provides
+ fun create(
+ interactor: SelectablePreviewsInteractor,
+ @PayloadToggle imageLoader: ImageLoader,
+ actionsInteractor: CustomActionsInteractor,
+ headlineGenerator: HeadlineGenerator,
+ selectionInteractor: SelectionInteractor,
+ // TODO: remove if possible
+ @ViewModelOwned scope: CoroutineScope,
+ ): ShareouselViewModel {
+ val keySet =
+ interactor.previews.stateIn(
+ scope,
+ SharingStarted.Eagerly,
+ initialValue = null,
+ )
+ return ShareouselViewModel(
+ headline =
+ selectionInteractor.amountSelected.map { numItems ->
+ val contentType = ContentType.Image // TODO: convert from metadata
+ when (contentType) {
+ ContentType.Other -> headlineGenerator.getFilesHeadline(numItems)
+ ContentType.Image -> headlineGenerator.getImagesHeadline(numItems)
+ ContentType.Video -> headlineGenerator.getVideosHeadline(numItems)
+ }
+ },
+ previews = keySet,
+ actions =
+ actionsInteractor.customActions.map { actions ->
+ actions.mapIndexedNotNull { i, model ->
+ val icon = model.icon
+ val label = model.label
+ if (icon == null && label.isBlank()) {
+ null
+ } else {
+ ActionChipViewModel(
+ label = label.toString(),
+ icon = model.icon,
+ onClicked = { model.performAction(i) },
+ )
+ }
+ }
+ },
+ preview = { key ->
+ keySet.value?.maybeLoad(key)
+ val previewInteractor = interactor.preview(key)
+ ShareouselPreviewViewModel(
+ bitmap = flow { emit(imageLoader(key.uri)) },
+ contentType = flowOf(ContentType.Image), // TODO: convert from metadata
+ isSelected = previewInteractor.isSelected,
+ setSelected = previewInteractor::setSelected,
+ )
+ },
+ )
+ }
+
+ @Provides
+ @PayloadToggle
+ fun imageLoader(
+ @ViewModelOwned viewModelScope: CoroutineScope,
+ @Background coroutineDispatcher: CoroutineDispatcher,
+ @ApplicationContext context: Context,
+ ): ImageLoader =
+ ImagePreviewImageLoader(
+ viewModelScope + coroutineDispatcher,
+ thumbnailSize =
+ context.resources.getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen),
+ context.contentResolver,
+ cacheSize = 16,
+ )
+}
+
+private fun PreviewsModel.maybeLoad(key: PreviewModel) {
+ when (key) {
+ previewModels.firstOrNull() -> loadMoreLeft?.invoke()
+ previewModels.lastOrNull() -> loadMoreRight?.invoke()
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/viewmodel/ShareouselViewModel.kt b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/viewmodel/ShareouselViewModel.kt
deleted file mode 100644
index 18ee2539..00000000
--- a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/viewmodel/ShareouselViewModel.kt
+++ /dev/null
@@ -1,90 +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.shareousel.ui.viewmodel
-
-import android.graphics.Bitmap
-import androidx.core.graphics.drawable.toBitmap
-import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory
-import com.android.intentresolver.contentpreview.ImageLoader
-import com.android.intentresolver.contentpreview.MutableActionFactory
-import com.android.intentresolver.contentpreview.PayloadToggleInteractor
-import com.android.intentresolver.icon.BitmapIcon
-import com.android.intentresolver.icon.ComposeIcon
-import com.android.intentresolver.widget.ActionRow.Action
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-data class ShareouselViewModel(
- val headline: Flow<String>,
- val previewKeys: StateFlow<List<Any>>,
- val actions: Flow<List<ActionChipViewModel>>,
- val centerIndex: StateFlow<Int>,
- val previewForKey: (key: Any) -> ShareouselImageViewModel,
- val previewRowKey: (Any) -> Any
-)
-
-data class ActionChipViewModel(val label: String, val icon: ComposeIcon?, val onClick: () -> Unit)
-
-data class ShareouselImageViewModel(
- val bitmap: Flow<Bitmap?>,
- val contentDescription: Flow<String>,
- val isSelected: Flow<Boolean>,
- val setSelected: (Boolean) -> Unit,
-)
-
-suspend fun PayloadToggleInteractor.toShareouselViewModel(
- imageLoader: ImageLoader,
- actionFactory: ActionFactory,
- scope: CoroutineScope,
-): ShareouselViewModel {
- return ShareouselViewModel(
- headline = MutableStateFlow("Shareousel"),
- previewKeys = previewKeys.stateIn(scope),
- actions =
- if (actionFactory is MutableActionFactory) {
- actionFactory.customActionsFlow.map { actions ->
- actions.map { it.toActionChipViewModel() }
- }
- } else {
- flow {
- emit(actionFactory.createCustomActions().map { it.toActionChipViewModel() })
- }
- },
- centerIndex = targetPosition.stateIn(scope),
- previewForKey = { key ->
- val previewInteractor = previewInteractor(key)
- ShareouselImageViewModel(
- bitmap = previewInteractor.previewUri.map { uri -> uri?.let { imageLoader(uri) } },
- contentDescription = MutableStateFlow(""),
- isSelected = previewInteractor.selected,
- setSelected = { isSelected -> previewInteractor.setSelected(isSelected) },
- )
- },
- previewRowKey = { getKey(it) },
- )
-}
-
-private fun Action.toActionChipViewModel() =
- ActionChipViewModel(
- label?.toString() ?: "",
- icon?.let { BitmapIcon(it.toBitmap()) },
- onClick = { onClicked.run() }
- )
diff --git a/java/src/com/android/intentresolver/inject/Qualifiers.kt b/java/src/com/android/intentresolver/inject/Qualifiers.kt
index 157e8f76..f267328b 100644
--- a/java/src/com/android/intentresolver/inject/Qualifiers.kt
+++ b/java/src/com/android/intentresolver/inject/Qualifiers.kt
@@ -23,6 +23,11 @@ import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
+annotation class ViewModelOwned
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
annotation class ApplicationOwned
@Qualifier
diff --git a/java/src/com/android/intentresolver/inject/ViewModelCoroutineScopeModule.kt b/java/src/com/android/intentresolver/inject/ViewModelCoroutineScopeModule.kt
new file mode 100644
index 00000000..4dda2653
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/ViewModelCoroutineScopeModule.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.inject
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.ViewModelLifecycle
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.scopes.ViewModelScoped
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object ViewModelCoroutineScopeModule {
+ @Provides
+ @ViewModelScoped
+ @ViewModelOwned
+ fun viewModelScope(@Main dispatcher: CoroutineDispatcher, lifecycle: ViewModelLifecycle) =
+ lifecycle.asCoroutineScope(dispatcher)
+}
+
+fun ViewModelLifecycle.asCoroutineScope(context: CoroutineContext = EmptyCoroutineContext) =
+ CoroutineScope(context).also { addOnClearedListener { it.cancel() } }
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java
index a95caddc..9a5ec173 100644
--- a/java/src/com/android/intentresolver/v2/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java
@@ -29,7 +29,6 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE
import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
-import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION;
import static com.android.intentresolver.v2.ext.CreationExtrasExtKt.addDefaultArgs;
import static com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL;
import static com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.PROFILE_WORK;
@@ -118,7 +117,6 @@ import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.BasePreviewViewModel;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
-import com.android.intentresolver.contentpreview.PayloadToggleInteractor;
import com.android.intentresolver.contentpreview.PreviewViewModel;
import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;
import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
@@ -608,33 +606,14 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
.get(BasePreviewViewModel.class);
previewViewModel.init(
mRequest.getTargetIntent(),
- mViewModel.getActivityModel().getIntent(),
mRequest.getAdditionalContentUri(),
- mRequest.getFocusedItemPosition(),
mChooserServiceFeatureFlags.chooserPayloadToggling());
- ChooserActionFactory chooserActionFactory = createChooserActionFactory();
- ChooserContentPreviewUi.ActionFactory actionFactory = chooserActionFactory;
- if (previewViewModel.getPreviewDataProvider().getPreviewType()
- == CONTENT_PREVIEW_PAYLOAD_SELECTION
- && mChooserServiceFeatureFlags.chooserPayloadToggling()) {
- PayloadToggleInteractor payloadToggleInteractor =
- previewViewModel.getPayloadToggleInteractor();
- if (payloadToggleInteractor != null) {
- ChooserMutableActionFactory mutableActionFactory =
- new ChooserMutableActionFactory(chooserActionFactory);
- actionFactory = mutableActionFactory;
- JavaFlowHelper.collect(
- getCoroutineScope(getLifecycle()),
- payloadToggleInteractor.getCustomActions(),
- mutableActionFactory::updateCustomActions);
- }
- }
mChooserContentPreviewUi = new ChooserContentPreviewUi(
getCoroutineScope(getLifecycle()),
previewViewModel.getPreviewDataProvider(),
mRequest.getTargetIntent(),
previewViewModel.getImageLoader(),
- actionFactory,
+ createChooserActionFactory(),
mEnterTransitionAnimationDelegate,
new HeadlineGeneratorImpl(this),
mRequest.getContentTypeHint(),
diff --git a/java/src/com/android/intentresolver/v2/ChooserMutableActionFactory.kt b/java/src/com/android/intentresolver/v2/ChooserMutableActionFactory.kt
deleted file mode 100644
index 2f8ccf77..00000000
--- a/java/src/com/android/intentresolver/v2/ChooserMutableActionFactory.kt
+++ /dev/null
@@ -1,54 +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
-
-import android.service.chooser.ChooserAction
-import com.android.intentresolver.contentpreview.ChooserContentPreviewUi
-import com.android.intentresolver.contentpreview.MutableActionFactory
-import com.android.intentresolver.widget.ActionRow
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-
-/** A wrapper around [ChooserActionFactory] that provides observable custom actions */
-class ChooserMutableActionFactory(
- private val actionFactory: ChooserActionFactory,
-) : MutableActionFactory, ChooserContentPreviewUi.ActionFactory by actionFactory {
- private val customActions =
- MutableStateFlow<List<ActionRow.Action>>(actionFactory.createCustomActions())
-
- override val customActionsFlow: Flow<List<ActionRow.Action>>
- get() = customActions
-
- override fun updateCustomActions(actions: List<ChooserAction>) {
- customActions.tryEmit(mapChooserActions(actions))
- }
-
- override fun createCustomActions(): List<ActionRow.Action> = customActions.value
-
- private fun mapChooserActions(chooserActions: List<ChooserAction>): List<ActionRow.Action> =
- buildList(chooserActions.size) {
- chooserActions.forEachIndexed { i, chooserAction ->
- val actionRow =
- actionFactory.createCustomAction(chooserAction) {
- actionFactory.logCustomAction(i)
- }
- if (actionRow != null) {
- add(actionRow)
- }
- }
- }
-}