diff options
author | 2024-05-24 15:23:55 +0000 | |
---|---|---|
committer | 2024-05-24 15:23:55 +0000 | |
commit | ad07ab8f46bbaba16b00c76960f6421312b38689 (patch) | |
tree | 26934d39f202f5de8380afb5834b904bd7426674 | |
parent | 160289301fe214fa092f43cb0581a8688dca2487 (diff) | |
parent | 7bd1caf07c36f117bccd8bd462e3b9491b91acd5 (diff) |
Merge "Honor content types in shareousel" into main
9 files changed, 128 insertions, 29 deletions
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 802e58a2..e99aa50c 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,8 +16,10 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor +import com.android.intentresolver.contentpreview.MimeTypeClassifier import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier +import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -31,6 +33,7 @@ constructor( private val selectionsRepo: PreviewSelectionsRepository, private val targetIntentModifier: TargetIntentModifier<PreviewModel>, private val updateTargetIntentInteractor: UpdateTargetIntentInteractor, + private val mimeTypeClassifier: MimeTypeClassifier, ) { /** Set of selected previews. */ val selections: StateFlow<Set<PreviewModel>> @@ -39,6 +42,8 @@ constructor( /** Amount of selected previews. */ val amountSelected: Flow<Int> = selectionsRepo.selections.map { it.size } + val aggregateContentType: Flow<ContentType> = selections.map { aggregateContentType(it) } + fun select(model: PreviewModel) { updateChooserRequest(selectionsRepo.selections.updateAndGet { it + model }) } @@ -53,4 +58,29 @@ constructor( val intent = targetIntentModifier.intentFromSelection(selections) updateTargetIntentInteractor.updateTargetIntent(intent) } + + private fun aggregateContentType( + items: Set<PreviewModel>, + ): ContentType { + if (items.isEmpty()) { + return ContentType.Other + } + + var allImages = true + var allVideos = true + for (item in items) { + allImages = allImages && mimeTypeClassifier.isImageType(item.mimeType) + allVideos = allVideos && mimeTypeClassifier.isVideoType(item.mimeType) + + if (!allImages && !allVideos) { + break + } + } + + return when { + allImages -> ContentType.Image + allVideos -> ContentType.Video + else -> ContentType.Other + } + } } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/ContentType.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/ContentType.kt new file mode 100644 index 00000000..3ef6d98f --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/ContentType.kt @@ -0,0 +1,24 @@ +/* + * 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.shared + +/** Type of the content being previewed. */ +enum class ContentType { + Image, + Video, + Other +} diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt index c2330ad8..a0be1a9b 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt @@ -33,7 +33,7 @@ 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 +import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType @Composable fun ShareouselCard( diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt index 36c94b59..c25b0154 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt @@ -56,8 +56,8 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.intentresolver.R +import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType 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 @@ -114,12 +114,10 @@ private fun PreviewCarousel( private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) { val bitmap by viewModel.bitmap.collectAsStateWithLifecycle(initialValue = null) val selected by viewModel.isSelected.collectAsStateWithLifecycle(initialValue = false) - val contentType by - viewModel.contentType.collectAsStateWithLifecycle(initialValue = ContentType.Image) val borderColor = MaterialTheme.colorScheme.primary val scope = rememberCoroutineScope() val contentDescription = - when (contentType) { + when (viewModel.contentType) { ContentType.Image -> stringResource(R.string.selectable_image) ContentType.Video -> stringResource(R.string.selectable_video) else -> stringResource(R.string.selectable_item) @@ -141,7 +139,7 @@ private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) { Box(modifier = Modifier.fillMaxHeight().aspectRatio(aspectRatio)) } }, - contentType = contentType, + contentType = viewModel.contentType, selected = selected, modifier = Modifier.thenIf(selected) { 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 index 9827fcd4..540229c9 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt @@ -17,6 +17,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel import android.graphics.Bitmap +import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType import kotlinx.coroutines.flow.Flow /** An individual preview within Shareousel. */ @@ -24,17 +25,10 @@ data class ShareouselPreviewViewModel( /** Image to be shared. */ val bitmap: Flow<Bitmap?>, /** Type of data to be shared. */ - val contentType: Flow<ContentType>, + val contentType: 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, val aspectRatio: Float, ) - -/** 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 index 1b9c231b..4eda3fa9 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt @@ -18,11 +18,13 @@ package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel import com.android.intentresolver.contentpreview.CachingImagePreviewImageLoader import com.android.intentresolver.contentpreview.HeadlineGenerator import com.android.intentresolver.contentpreview.ImageLoader +import com.android.intentresolver.contentpreview.MimeTypeClassifier import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggle import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.ChooserRequestInteractor 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.ContentType import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel import com.android.intentresolver.inject.ViewModelOwned @@ -35,9 +37,9 @@ 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.flow.zip /** A dynamic carousel of selectable previews within share sheet. */ data class ShareouselViewModel( @@ -71,6 +73,7 @@ interface ShareouselViewModelModule { headlineGenerator: HeadlineGenerator, selectionInteractor: SelectionInteractor, chooserRequestInteractor: ChooserRequestInteractor, + mimeTypeClassifier: MimeTypeClassifier, // TODO: remove if possible @ViewModelOwned scope: CoroutineScope, ): ShareouselViewModel { @@ -82,8 +85,9 @@ interface ShareouselViewModelModule { ) return ShareouselViewModel( headline = - selectionInteractor.amountSelected.map { numItems -> - val contentType = ContentType.Image // TODO: convert from metadata + selectionInteractor.aggregateContentType.zip( + selectionInteractor.amountSelected + ) { contentType, numItems -> when (contentType) { ContentType.Other -> headlineGenerator.getFilesHeadline(numItems) ContentType.Image -> headlineGenerator.getImagesHeadline(numItems) @@ -111,9 +115,15 @@ interface ShareouselViewModelModule { preview = { key -> keySet.value?.maybeLoad(key) val previewInteractor = interactor.preview(key) + val contentType = + when { + mimeTypeClassifier.isImageType(key.mimeType) -> ContentType.Image + mimeTypeClassifier.isVideoType(key.mimeType) -> ContentType.Video + else -> ContentType.Other + } ShareouselPreviewViewModel( bitmap = flow { emit(key.previewUri?.let { imageLoader(it) }) }, - contentType = flowOf(ContentType.Image), // TODO: convert from metadata + contentType = contentType, isSelected = previewInteractor.isSelected, setSelected = previewInteractor::setSelected, aspectRatio = key.aspectRatio, diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt index 659c178c..8f7c59de 100644 --- a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt +++ b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt @@ -20,6 +20,7 @@ import com.android.intentresolver.backgroundDispatcher import com.android.intentresolver.contentResolver import com.android.intentresolver.contentpreview.HeadlineGenerator import com.android.intentresolver.contentpreview.ImageLoader +import com.android.intentresolver.contentpreview.mimetypeClassifier import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.activityResultRepository import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.pendingSelectionCallbackRepository @@ -97,6 +98,7 @@ val Kosmos.selectionInteractor selectionsRepo = previewSelectionsRepository, targetIntentModifier = targetIntentModifier, updateTargetIntentInteractor = updateTargetIntentInteractor, + mimeTypeClassifier = mimetypeClassifier, ) val Kosmos.setCursorPreviewsInteractor diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt index a64807b7..708e6cc6 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt @@ -18,6 +18,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interacto import android.content.Intent 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.shared.model.PreviewModel import com.android.intentresolver.util.runKosmosTest @@ -35,7 +36,8 @@ class SelectionInteractorTest { SelectionInteractor( previewSelectionsRepository, { Intent() }, - updateTargetIntentInteractor + updateTargetIntentInteractor, + mimetypeClassifier, ) assertThat(underTest.selections.value).isEqualTo(setOf(initialPreview)) @@ -57,7 +59,8 @@ class SelectionInteractorTest { SelectionInteractor( previewSelectionsRepository, { Intent() }, - updateTargetIntentInteractor + updateTargetIntentInteractor, + mimetypeClassifier ) underTest.unselect(first) diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt index bd3d88f8..fb3e9a3f 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt @@ -25,6 +25,7 @@ import android.graphics.drawable.Icon import android.net.Uri import com.android.intentresolver.FakeImageLoader import com.android.intentresolver.contentpreview.HeadlineGenerator +import com.android.intentresolver.contentpreview.mimetypeClassifier 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.cursorPreviewsRepository @@ -39,6 +40,7 @@ import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.payloadToggleImageLoader 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.ContentType import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel import com.android.intentresolver.data.model.ChooserRequest @@ -68,23 +70,24 @@ class ShareouselViewModelTest { actionsInteractor = customActionsInteractor, headlineGenerator = headlineGenerator, chooserRequestInteractor = chooserRequestInteractor, + mimeTypeClassifier = mimetypeClassifier, selectionInteractor = selectionInteractor, scope = viewModelScope, ) } @Test - fun headline() = runTest { - assertThat(shareouselViewModel.headline.first()).isEqualTo("IMAGES: 1") + fun headline_images() = runTest { + assertThat(shareouselViewModel.headline.first()).isEqualTo("FILES: 1") previewSelectionsRepository.selections.value = setOf( PreviewModel( uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = null, + mimeType = "image/png", ), PreviewModel( uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = null, + mimeType = "image/jpeg", ) ) runCurrent() @@ -92,6 +95,40 @@ class ShareouselViewModelTest { } @Test + fun headline_videos() = runTest { + previewSelectionsRepository.selections.value = + setOf( + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = "video/mpeg", + ), + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "video/mpeg", + ) + ) + runCurrent() + assertThat(shareouselViewModel.headline.first()).isEqualTo("VIDEOS: 2") + } + + @Test + fun headline_mixed() = runTest { + previewSelectionsRepository.selections.value = + setOf( + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = "image/jpeg", + ), + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "video/mpeg", + ) + ) + runCurrent() + assertThat(shareouselViewModel.headline.first()).isEqualTo("FILES: 2") + } + + @Test fun metadataText() = runTest { val request = ChooserRequest( @@ -115,11 +152,11 @@ class ShareouselViewModelTest { setOf( PreviewModel( uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = null, + mimeType = "image/png", ), PreviewModel( uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = null, + mimeType = "video/mpeg", ) ), startIdx = 1, @@ -143,12 +180,13 @@ class ShareouselViewModelTest { shareouselViewModel.preview( PreviewModel( uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = null + mimeType = "video/mpeg" ) ) assertWithMessage("preview bitmap is null").that(previewVm.bitmap.first()).isNotNull() assertThat(previewVm.isSelected.first()).isFalse() + assertThat(previewVm.contentType).isEqualTo(ContentType.Video) previewVm.setSelected(true) @@ -234,9 +272,9 @@ class ShareouselViewModelTest { override fun getFilesWithTextHeadline(text: CharSequence, count: Int): String = error("not supported") - override fun getVideosHeadline(count: Int): String = error("not supported") + override fun getVideosHeadline(count: Int): String = "VIDEOS: $count" - override fun getFilesHeadline(count: Int): String = error("not supported") + override fun getFilesHeadline(count: Int): String = "FILES: $count" } // instantiate the view model, and then runCurrent() so that it is fully hydrated before // starting the test |