From 1975528de9f1abcbfcebd4a4dadbf9858e9fe764 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Tue, 4 Jun 2024 10:46:04 -0700 Subject: Shareousel: Maintain cursor order for shated items Add a position property to PreviewModel class to track relative order of items. For each item, the initial value is artificial and derived from the order of the initially shared items and is updated upon reading the additional items cursor. Upon sharing, If the selection has not change, the items will be shared in their original order; If the selection has changed, the order of the items will be affected by the observed items order in the cursor. Fix: 329683774 Test: manual testing Test: atest IntentResolver-tests-unit Test: atest IntentResolver-tests-activity Flag: android.service.chooser.chooser_payload_toggling Change-Id: Ie552887702cde75cb1a05ed3ec5415f4f4a5c8dc --- .../data/repository/PreviewSelectionsRepository.kt | 3 +- .../domain/cursor/PayloadToggleCursorResolver.kt | 2 +- .../domain/interactor/CursorPreviewsInteractor.kt | 32 ++++++++++++++-------- .../domain/interactor/FetchPreviewsInteractor.kt | 14 ++++++---- .../interactor/SelectablePreviewInteractor.kt | 2 +- .../domain/interactor/SelectionInteractor.kt | 30 ++++++++++++++------ .../payloadtoggle/domain/model/CursorRow.kt | 2 +- .../payloadtoggle/shared/model/PreviewModel.kt | 4 +++ .../intentresolver/util/ParallelIteration.kt | 21 ++++++++++++++ 9 files changed, 80 insertions(+), 30 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt index 48c06192..81c56d1e 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt @@ -16,6 +16,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.data.repository +import android.net.Uri import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import dagger.hilt.android.scopes.ViewModelScoped import javax.inject.Inject @@ -24,5 +25,5 @@ import kotlinx.coroutines.flow.MutableStateFlow /** Stores set of selected previews. */ @ViewModelScoped class PreviewSelectionsRepository @Inject constructor() { - val selections = MutableStateFlow(emptyList()) + val selections = MutableStateFlow(emptyMap()) } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolver.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolver.kt index d9612696..148310e6 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolver.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolver.kt @@ -55,7 +55,7 @@ constructor( ) } .getOrNull() - ?.viewBy { readUri()?.let { uri -> CursorRow(uri, readSize()) } } + ?.viewBy { readUri()?.let { uri -> CursorRow(uri, readSize(), position) } } } private fun Cursor.readUri(): Uri? { diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt index fa600c86..a475263c 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt @@ -56,6 +56,7 @@ class CursorPreviewsInteractor @Inject constructor( private val interactor: SetCursorPreviewsInteractor, + private val selectionInteractor: SelectionInteractor, @FocusedItemIndex private val focusedItemIdx: Int, private val uriMetadataReader: UriMetadataReader, @PageSize private val pageSize: Int, @@ -287,19 +288,26 @@ constructor( private fun createPreviewModel( row: CursorRow, unclaimedRecords: MutableUnclaimedMap, - ): PreviewModel = - unclaimedRecords.remove(row.uri)?.second - ?: uriMetadataReader.getMetadata(row.uri).let { metadata -> - val size = - row.previewSize - ?: metadata.previewUri?.let { uriMetadataReader.readPreviewSize(it) } - PreviewModel( - uri = row.uri, - previewUri = metadata.previewUri, - mimeType = metadata.mimeType, - aspectRatio = size.aspectRatioOrDefault(1f), - ) + ): PreviewModel = uriMetadataReader.getMetadata(row.uri).let { metadata -> + val size = + row.previewSize + ?: metadata.previewUri?.let { uriMetadataReader.readPreviewSize(it) } + PreviewModel( + uri = row.uri, + previewUri = metadata.previewUri, + mimeType = metadata.mimeType, + aspectRatio = size.aspectRatioOrDefault(1f), + order = row.position, + ) + }.also { updated -> + if (unclaimedRecords.remove(row.uri) != null) { + // unclaimedRecords contains initially shared (and thus selected) items with unknown + // cursor position. Update selection records when any of those items is encountered + // in the cursor to maintain proper selection order should other items also be + // selected. + selectionInteractor.updateSelection(updated) } + } private fun M.putAllUnclaimedRight(unclaimed: UnclaimedMap): M = putAllUnclaimedWhere(unclaimed) { it >= focusedItemIdx } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt index c9c9a9b3..50086a23 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt @@ -25,7 +25,7 @@ import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.Curs import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import com.android.intentresolver.inject.ContentUris import com.android.intentresolver.inject.FocusedItemIndex -import com.android.intentresolver.util.mapParallel +import com.android.intentresolver.util.mapParallelIndexed import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -45,7 +45,7 @@ constructor( suspend fun activate() = coroutineScope { val cursor = async { cursorResolver.getCursor() } val initialPreviewMap = getInitialPreviews() - selectionRepository.selections.value = initialPreviewMap + selectionRepository.selections.value = initialPreviewMap.associateBy { it.uri } setCursorPreviews.setPreviews( previews = initialPreviewMap, startIndex = focusedItemIdx, @@ -61,7 +61,7 @@ constructor( selectedItems // Restrict parallelism so as to not overload the metadata reader; anecdotally, too // many parallel queries causes failures. - .mapParallel(parallelism = 4) { uri -> + .mapParallelIndexed(parallelism = 4) { index, uri -> val metadata = uriMetadataReader.getMetadata(uri) PreviewModel( uri = uri, @@ -70,8 +70,12 @@ constructor( aspectRatio = metadata.previewUri?.let { uriMetadataReader.readPreviewSize(it).aspectRatioOrDefault(1f) - } - ?: 1f, + } ?: 1f, + order = when { + index < focusedItemIdx -> Int.MIN_VALUE + index + index == focusedItemIdx -> 0 + else -> Int.MAX_VALUE - selectedItems.size + index + 1 + } ) } } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt index 55a995f5..d52a71a1 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt @@ -29,7 +29,7 @@ class SelectablePreviewInteractor( val uri: Uri = key.uri /** Whether or not this preview is selected by the user. */ - val isSelected: Flow = selectionInteractor.selections.map { key in it } + val isSelected: Flow = selectionInteractor.selections.map { key.uri in it } /** Sets whether this preview is selected by the user. */ fun setSelected(isSelected: Boolean) { diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt index 13af92cb..97d9fa66 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt @@ -16,6 +16,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor +import android.net.Uri import com.android.intentresolver.contentpreview.MimeTypeClassifier import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier @@ -23,8 +24,9 @@ import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentTyp import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet class SelectionInteractor @@ -36,31 +38,41 @@ constructor( private val mimeTypeClassifier: MimeTypeClassifier, ) { /** List of selected previews. */ - val selections: StateFlow> - get() = selectionsRepo.selections + val selections: Flow> = + selectionsRepo.selections.map { it.keys }.distinctUntilChanged() /** Amount of selected previews. */ val amountSelected: Flow = selectionsRepo.selections.map { it.size } - val aggregateContentType: Flow = selections.map { aggregateContentType(it) } + val aggregateContentType: Flow = + selectionsRepo.selections.map { aggregateContentType(it.values) } + + fun updateSelection(model: PreviewModel) { + selectionsRepo.selections.update { + if (it.containsKey(model.uri)) it + (model.uri to model) else it + } + } fun select(model: PreviewModel) { - updateChooserRequest(selectionsRepo.selections.updateAndGet { it + model }) + updateChooserRequest( + selectionsRepo.selections.updateAndGet { it + (model.uri to model) }.values + ) } fun unselect(model: PreviewModel) { if (selectionsRepo.selections.value.size > 1) { - updateChooserRequest(selectionsRepo.selections.updateAndGet { it - model }) + updateChooserRequest(selectionsRepo.selections.updateAndGet { it - model.uri }.values) } } - private fun updateChooserRequest(selections: List) { - val intent = targetIntentModifier.intentFromSelection(selections) + private fun updateChooserRequest(selections: Collection) { + val sorted = selections.sortedBy { it.order } + val intent = targetIntentModifier.intentFromSelection(sorted) updateTargetIntentInteractor.updateTargetIntent(intent) } private fun aggregateContentType( - items: List, + items: Collection, ): ContentType { if (items.isEmpty()) { return ContentType.Other diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/CursorRow.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/CursorRow.kt index f1d856ac..aae29102 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/CursorRow.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/CursorRow.kt @@ -20,4 +20,4 @@ import android.net.Uri import android.util.Size /** Represents additional content cursor row */ -data class CursorRow(val uri: Uri, val previewSize: Size?) +data class CursorRow(val uri: Uri, val previewSize: Size?, val position: Int) diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt index 85c70004..8a479156 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt @@ -27,4 +27,8 @@ data class PreviewModel( /** Mimetype for the data [uri] points to. */ val mimeType: String?, val aspectRatio: Float = 1f, + /** + * Relative item position in the list that is used to determine items order in the target intent + */ + val order: Int, ) diff --git a/java/src/com/android/intentresolver/util/ParallelIteration.kt b/java/src/com/android/intentresolver/util/ParallelIteration.kt index 70c46c47..745bcdbf 100644 --- a/java/src/com/android/intentresolver/util/ParallelIteration.kt +++ b/java/src/com/android/intentresolver/util/ParallelIteration.kt @@ -48,3 +48,24 @@ private suspend fun Iterable.mapParallel(block: suspend (A) -> B): Lis } .awaitAll() } + +suspend fun Iterable.mapParallelIndexed( + parallelism: Int? = null, + block: suspend (Int, A) -> B, +): List = + parallelism?.let { permits -> + withSemaphore(permits = permits) { + mapParallelIndexed { idx, item -> withPermit { block(idx, item) } } + } + } ?: mapParallelIndexed(block) + +private suspend fun Iterable.mapParallelIndexed(block: suspend (Int, A) -> B): List = + coroutineScope { + mapIndexed { index, item -> + async { + yield() + block(index, item) + } + } + .awaitAll() + } -- cgit v1.2.3-59-g8ed1b