diff options
author | 2024-05-13 20:48:53 +0000 | |
---|---|---|
committer | 2024-05-13 20:48:53 +0000 | |
commit | 25c04565081fd8643e04f88e004fc09ac1c73ff3 (patch) | |
tree | 584db894c6a903ade28ee3636fd8ab8bcb06b8a6 | |
parent | a487bbfeff7fcb4d0475e9d9aa4307539feacca2 (diff) | |
parent | 78359ff22fae3934e513ba8c498af7e8a48992fc (diff) |
Merge "Read image size from URI metadata" into main
11 files changed, 163 insertions, 67 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/FileInfo.kt b/java/src/com/android/intentresolver/contentpreview/FileInfo.kt index fe35365b..16a948df 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileInfo.kt +++ b/java/src/com/android/intentresolver/contentpreview/FileInfo.kt @@ -22,8 +22,11 @@ class FileInfo private constructor(val uri: Uri, val previewUri: Uri?, val mimeT @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) class Builder(val uri: Uri) { var previewUri: Uri? = null + @Synchronized get private set + var mimeType: String? = null + @Synchronized get private set @Synchronized fun withPreviewUri(uri: Uri?): Builder = apply { previewUri = uri } diff --git a/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt b/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt index b5361889..4e403c22 100644 --- a/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt +++ b/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt @@ -20,6 +20,8 @@ import android.content.ContentInterface import android.media.MediaMetadata import android.net.Uri import android.provider.DocumentsContract +import android.provider.MediaStore.MediaColumns +import android.util.Size import dagger.Binds import dagger.Module import dagger.Provides @@ -29,6 +31,7 @@ import javax.inject.Inject fun interface UriMetadataReader { fun getMetadata(uri: Uri): FileInfo + fun readPreviewSize(uri: Uri): Size? = null } class UriMetadataReaderImpl @@ -56,6 +59,8 @@ constructor( return builder.build() } + override fun readPreviewSize(uri: Uri): Size? = contentResolver.readPreviewSize(uri) + private fun ContentInterface.supportsImageType(uri: Uri): Boolean = getStreamTypesSafe(uri).firstOrNull { typeClassifier.isImageType(it) } != null @@ -73,6 +78,15 @@ constructor( null } } + + private fun ContentInterface.readPreviewSize(uri: Uri): Size? = + querySafe(uri, arrayOf(MediaColumns.WIDTH, MediaColumns.HEIGHT))?.use { cursor -> + if (cursor.moveToFirst()) { + cursor.readSize() + } else { + null + } + } } @Module 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 c7d29a72..97b087e1 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 @@ -242,10 +242,14 @@ constructor( ): 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), ) } 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 c87504e1..80cd03d9 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 @@ -65,6 +65,11 @@ constructor( uri = uri, previewUri = metadata.previewUri, mimeType = metadata.mimeType, + aspectRatio = + metadata.previewUri?.let { + uriMetadataReader.readPreviewSize(it).aspectRatioOrDefault(1f) + } + ?: 1f, ) } .toSet() diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SizeExtensions.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SizeExtensions.kt new file mode 100644 index 00000000..4cf10414 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SizeExtensions.kt @@ -0,0 +1,26 @@ +/* + * 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.payloadtoggle.domain.interactor + +import android.util.Size + +internal fun Size?.aspectRatioOrDefault(default: Float): Float = + when { + this == null -> default + width >= 0 && height > 0 -> width.toFloat() / height.toFloat() + else -> default + } 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 6b805391..85c70004 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 @@ -26,4 +26,5 @@ data class PreviewModel( val previewUri: Uri? = uri, /** Mimetype for the data [uri] points to. */ val mimeType: String?, + val aspectRatio: Float = 1f, ) 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 0a431c2a..85ad6ab3 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 @@ -115,11 +115,9 @@ private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) { val scope = rememberCoroutineScope() ShareouselCard( image = { + // TODO: max ratio is actually equal to the viewport ratio + val aspectRatio = viewModel.aspectRatio.coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) bitmap?.let { bitmap -> - val aspectRatio = - (bitmap.width.toFloat() / bitmap.height.toFloat()) - // TODO: max ratio is actually equal to the viewport ratio - .coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) Image( bitmap = bitmap.asImageBitmap(), contentDescription = null, @@ -129,7 +127,7 @@ private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) { } ?: run { // TODO: look at ScrollableImagePreviewView.setLoading() - Box(modifier = Modifier.fillMaxHeight().aspectRatio(2f / 5f)) + Box(modifier = Modifier.fillMaxHeight().aspectRatio(aspectRatio)) } }, contentType = contentType, 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 a245b3e3..9827fcd4 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 @@ -29,6 +29,7 @@ data class ShareouselPreviewViewModel( 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. */ 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 cf118934..8b2dd818 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 @@ -117,6 +117,7 @@ object ShareouselViewModelModule { contentType = flowOf(ContentType.Image), // TODO: convert from metadata isSelected = previewInteractor.isSelected, setSelected = previewInteractor::setSelected, + aspectRatio = key.aspectRatio, ) }, ) diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt index 9b786b74..ff699373 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt @@ -20,12 +20,16 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interacto import android.database.MatrixCursor import android.net.Uri +import android.provider.MediaStore.MediaColumns.HEIGHT +import android.provider.MediaStore.MediaColumns.WIDTH +import android.util.Size import androidx.core.os.bundleOf import com.android.intentresolver.contentpreview.FileInfo import com.android.intentresolver.contentpreview.UriMetadataReader import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.CursorRow import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel +import com.android.intentresolver.contentpreview.readSize import com.android.intentresolver.contentpreview.uriMetadataReader import com.android.intentresolver.util.KosmosTestScope import com.android.intentresolver.util.cursor.CursorView @@ -46,21 +50,32 @@ class CursorPreviewsInteractorTest { cursorStartPosition: Int = cursor.count() / 2, pageSize: Int = 16, maxLoadedPages: Int = 3, + cursorSizes: Map<Int, Size> = emptyMap(), + metadatSizes: Map<Int, Size> = emptyMap(), block: KosmosTestScope.(TestDeps) -> Unit, ) { + val metadataUriToSize = metadatSizes.mapKeys { uri(it.key) } with(Kosmos()) { this.focusedItemIndex = focusedItemIndex this.pageSize = pageSize this.maxLoadedPages = maxLoadedPages - uriMetadataReader = UriMetadataReader { - FileInfo.Builder(it).withPreviewUri(it).withMimeType("image/bitmap").build() - } + uriMetadataReader = + object : UriMetadataReader { + override fun getMetadata(uri: Uri): FileInfo = + FileInfo.Builder(uri) + .withPreviewUri(uri) + .withMimeType("image/bitmap") + .build() + + override fun readPreviewSize(uri: Uri): Size? = metadataUriToSize[uri] + } runTest { block( TestDeps( initialSelection, cursor, cursorStartPosition, + cursorSizes, ) ) } @@ -71,54 +86,66 @@ class CursorPreviewsInteractorTest { initialSelectionRange: Iterable<Int>, private val cursorRange: Iterable<Int>, private val cursorStartPosition: Int, + private val cursorSizes: Map<Int, Size>, ) { val cursor: CursorView<CursorRow?> = - MatrixCursor(arrayOf("uri")) + MatrixCursor(arrayOf("uri", WIDTH, HEIGHT)) .apply { extras = bundleOf("position" to cursorStartPosition) for (i in cursorRange) { - newRow().add("uri", uri(i).toString()) + val size = cursorSizes[i] + addRow( + arrayOf( + uri(i).toString(), + size?.width?.toString(), + size?.height?.toString(), + ) + ) } } - .viewBy { getString(0)?.let { uriStr -> CursorRow(Uri.parse(uriStr), null) } } + .viewBy { getString(0)?.let { uriStr -> CursorRow(Uri.parse(uriStr), readSize()) } } val initialPreviews: List<PreviewModel> = initialSelectionRange.map { i -> PreviewModel(uri = uri(i), mimeType = "image/bitmap") } - - private fun uri(index: Int) = Uri.fromParts("scheme$index", "ssp$index", "fragment$index") } @Test - fun initialCursorLoad() = runTestWithDeps { deps -> - backgroundScope.launch { - cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews) - } - runCurrent() + fun initialCursorLoad() = + runTestWithDeps( + cursorSizes = mapOf(0 to (200 x 100)), + metadatSizes = mapOf(0 to (300 x 100), 3 to (400 x 100)) + ) { deps -> + backgroundScope.launch { + cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews) + } + runCurrent() - assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull() - assertThat(cursorPreviewsRepository.previewsModel.value!!.startIdx).isEqualTo(0) - assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull() - assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNull() - assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels) - .containsExactly( - PreviewModel( - uri = Uri.fromParts("scheme0", "ssp0", "fragment0"), - mimeType = "image/bitmap" - ), - PreviewModel( - uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = "image/bitmap" - ), - PreviewModel( - uri = Uri.fromParts("scheme2", "ssp2", "fragment2"), - mimeType = "image/bitmap" - ), - PreviewModel( - uri = Uri.fromParts("scheme3", "ssp3", "fragment3"), - mimeType = "image/bitmap" - ), - ) - .inOrder() - } + assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull() + assertThat(cursorPreviewsRepository.previewsModel.value!!.startIdx).isEqualTo(0) + assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull() + assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNull() + assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels) + .containsExactly( + PreviewModel( + uri = Uri.fromParts("scheme0", "ssp0", "fragment0"), + mimeType = "image/bitmap", + aspectRatio = 2f, + ), + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "image/bitmap" + ), + PreviewModel( + uri = Uri.fromParts("scheme2", "ssp2", "fragment2"), + mimeType = "image/bitmap" + ), + PreviewModel( + uri = Uri.fromParts("scheme3", "ssp3", "fragment3"), + mimeType = "image/bitmap", + aspectRatio = 4f, + ), + ) + .inOrder() + } @Test fun loadMoreLeft_evictRight() = @@ -294,3 +321,7 @@ class CursorPreviewsInteractorTest { assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull() } } + +private fun uri(index: Int) = Uri.fromParts("scheme$index", "ssp$index", "fragment$index") + +private infix fun Int.x(height: Int) = Size(this, height) diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt index c9f71f49..735bcb1d 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt @@ -20,6 +20,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interacto import android.database.MatrixCursor import android.net.Uri +import android.util.Size import androidx.core.os.bundleOf import com.android.intentresolver.contentpreview.FileInfo import com.android.intentresolver.contentpreview.UriMetadataReader @@ -53,17 +54,26 @@ class FetchPreviewsInteractorTest { cursorStartPosition: Int = cursor.count() / 2, pageSize: Int = 16, maxLoadedPages: Int = 3, + previewSizes: Map<Int, Size> = emptyMap(), block: KosmosTestScope.() -> Unit, ) { + val previewUriToSize = previewSizes.mapKeys { uri(it.key) } with(Kosmos()) { fakeCursorResolver = FakeCursorResolver(cursorRange = cursor, cursorStartPosition = cursorStartPosition) payloadToggleCursorResolver = fakeCursorResolver contentUris = initialSelection.map { uri(it) } this.focusedItemIndex = focusedItemIndex - uriMetadataReader = UriMetadataReader { - FileInfo.Builder(it).withPreviewUri(it).withMimeType("image/bitmap").build() - } + uriMetadataReader = + object : UriMetadataReader { + override fun getMetadata(uri: Uri): FileInfo = + FileInfo.Builder(uri) + .withPreviewUri(uri) + .withMimeType("image/bitmap") + .build() + + override fun readPreviewSize(uri: Uri): Size? = previewUriToSize[uri] + } this.pageSize = pageSize this.maxLoadedPages = maxLoadedPages runKosmosTest { block() } @@ -94,30 +104,32 @@ class FetchPreviewsInteractorTest { } @Test - fun setsInitialPreviews() = runTest { - backgroundScope.launch { fetchPreviewsInteractor.activate() } - runCurrent() + fun setsInitialPreviews() = + runTest(previewSizes = mapOf(1 to Size(100, 50))) { + backgroundScope.launch { fetchPreviewsInteractor.activate() } + runCurrent() - assertThat(cursorPreviewsRepository.previewsModel.value) - .isEqualTo( - PreviewsModel( - previewModels = - setOf( - PreviewModel( - uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = "image/bitmap", - ), - PreviewModel( - uri = Uri.fromParts("scheme2", "ssp2", "fragment2"), - mimeType = "image/bitmap", + assertThat(cursorPreviewsRepository.previewsModel.value) + .isEqualTo( + PreviewsModel( + previewModels = + setOf( + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "image/bitmap", + aspectRatio = 2f + ), + PreviewModel( + uri = Uri.fromParts("scheme2", "ssp2", "fragment2"), + mimeType = "image/bitmap", + ), ), - ), - startIdx = 1, - loadMoreLeft = null, - loadMoreRight = null, + startIdx = 1, + loadMoreLeft = null, + loadMoreRight = null, + ) ) - ) - } + } @Test fun lookupCursorFromContentResolver() = runTest { |