summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2024-05-08 16:30:50 -0700
committer Andrey Epin <ayepin@google.com> 2024-05-10 14:12:30 -0700
commit78359ff22fae3934e513ba8c498af7e8a48992fc (patch)
tree27495dec0a6ebef71ba0062f41e80eba41cc479e
parent6b2802803702740646eb8f07bada2a4d2eec23b8 (diff)
Read image size from URI metadata
Read preview sizes from both URI metdata, and the additional content proivider response (the latter gets priority, when present); use them to set aspect ratio for the item preview. If the size is missing, fallback to default aspect ratio, 1:1. Bug: 339679442 Test: atest IntentResolver-tests-unit Test: manual testing with ShareTest app Change-Id: Ia6072620a79b5df0b4b4bc9ebd11fb3961cb18a6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FileInfo.kt3
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt14
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt4
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt5
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SizeExtensions.kt26
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt1
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt8
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt1
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt1
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt107
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt60
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 {