From 90bddf71b63f5082c4ec9e697b0baaacb5f81ecd Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Thu, 9 May 2024 12:08:06 -0700 Subject: Add support for preview size columns in the additional content query. Add support for MediaStore WIDTH and HEIGHT columns in the additional content query reponse. Parse those columns if they are present but do not actually use the values (yet). Bug: 339679442 Test: atest IntentResolver-tests-unit Test: atest IntentResolver-tests-activity Change-Id: I2a3ebc2c166d1cb9203824b2ac1bf0f9c4ec76da --- .../contentpreview/UriMetadataHelpers.kt | 22 +++++++++++ .../domain/cursor/PayloadToggleCursorResolver.kt | 27 +++++++++---- .../domain/interactor/CursorPreviewsInteractor.kt | 44 ++++++++++++---------- .../domain/interactor/FetchPreviewsInteractor.kt | 3 +- .../payloadtoggle/domain/model/CursorRow.kt | 23 +++++++++++ 5 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/CursorRow.kt (limited to 'java') diff --git a/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt b/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt index 41638b1f..c532b9a5 100644 --- a/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt +++ b/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt @@ -23,9 +23,12 @@ import android.net.Uri import android.provider.DocumentsContract import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL import android.provider.Downloads +import android.provider.MediaStore.MediaColumns.HEIGHT +import android.provider.MediaStore.MediaColumns.WIDTH import android.provider.OpenableColumns import android.text.TextUtils import android.util.Log +import android.util.Size import com.android.intentresolver.measurements.runTracing internal fun ContentInterface.getTypeSafe(uri: Uri): String? = @@ -83,6 +86,25 @@ internal fun Cursor.readPreviewUri(): Uri? = } .getOrNull() +fun Cursor.readSize(): Size? { + val widthIdx = columnNames.indexOf(WIDTH) + val heightIdx = columnNames.indexOf(HEIGHT) + return if (widthIdx < 0 || heightIdx < 0 || isNull(widthIdx) || isNull(heightIdx)) { + null + } else { + runCatching { + val width = getInt(widthIdx) + val height = getInt(heightIdx) + if (width >= 0 && height > 0) { + Size(width, height) + } else { + null + } + } + .getOrNull() + } +} + internal fun Cursor.readTitle(): String = runCatching { var nameColIndex = -1 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 3cf2af13..d9612696 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 @@ -16,11 +16,14 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor -import android.content.ContentResolver +import android.content.ContentInterface import android.content.Intent +import android.database.Cursor import android.net.Uri import android.service.chooser.AdditionalContentContract.Columns.URI import androidx.core.os.bundleOf +import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.CursorRow +import com.android.intentresolver.contentpreview.readSize import com.android.intentresolver.inject.AdditionalContent import com.android.intentresolver.inject.ChooserIntent import com.android.intentresolver.util.cursor.CursorView @@ -37,23 +40,31 @@ import javax.inject.Qualifier class PayloadToggleCursorResolver @Inject constructor( - private val contentResolver: ContentResolver, + private val contentResolver: ContentInterface, @AdditionalContent private val cursorUri: Uri, @ChooserIntent private val chooserIntent: Intent, -) : CursorResolver { - override suspend fun getCursor(): CursorView? = withCancellationSignal { signal -> +) : CursorResolver { + override suspend fun getCursor(): CursorView? = withCancellationSignal { signal -> runCatching { contentResolver.query( cursorUri, - arrayOf(URI), + // TODO: uncomment to start using that data + arrayOf(URI /*, WIDTH, HEIGHT*/), bundleOf(Intent.EXTRA_INTENT to chooserIntent), signal, ) } .getOrNull() - ?.viewBy { - getString(0)?.let(Uri::parse)?.takeIf { it.authority != cursorUri.authority } + ?.viewBy { readUri()?.let { uri -> CursorRow(uri, readSize()) } } + } + + private fun Cursor.readUri(): Uri? { + val uriIdx = columnNames.indexOf(URI) + if (uriIdx < 0) return null + return runCatching { + getString(uriIdx)?.let(Uri::parse)?.takeIf { it.authority != cursorUri.authority } } + .getOrNull() } @Module @@ -61,7 +72,7 @@ constructor( interface Binding { @Binds @PayloadToggle - fun bind(cursorResolver: PayloadToggleCursorResolver): CursorResolver + fun bind(cursorResolver: PayloadToggleCursorResolver): CursorResolver } } 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 f642f420..9d62ffa2 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 @@ -21,6 +21,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interacto import android.net.Uri import android.service.chooser.AdditionalContentContract.CursorExtraKeys.POSITION import com.android.intentresolver.contentpreview.UriMetadataReader +import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.CursorRow import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.LoadDirection import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.LoadedWindow import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.expandWindowLeft @@ -64,7 +65,7 @@ constructor( } /** Start reading data from [uriCursor], and listen for requests to load more. */ - suspend fun launch(uriCursor: CursorView, initialPreviews: Iterable) { + suspend fun launch(uriCursor: CursorView, initialPreviews: Iterable) { // Unclaimed values from the initial selection set. Entries will be removed as the cursor is // read, and any still present are inserted at the start / end of the cursor when it is // reached by the user. @@ -73,7 +74,7 @@ constructor( .asSequence() .mapIndexed { i, m -> Pair(m.uri, Pair(i, m)) } .toMap(ConcurrentHashMap()) - val pagedCursor: PagedCursor = uriCursor.paged(pageSize) + val pagedCursor: PagedCursor = uriCursor.paged(pageSize) val startPosition = uriCursor.extras?.getInt(POSITION, 0) ?: 0 val state = readInitialState(pagedCursor, startPosition, unclaimedRecords) processLoadRequests(state, pagedCursor, unclaimedRecords) @@ -82,7 +83,7 @@ constructor( /** Loop forever, processing any loading requests from the UI and updating local cache. */ private suspend fun processLoadRequests( initialState: CursorWindow, - pagedCursor: PagedCursor, + pagedCursor: PagedCursor, unclaimedRecords: MutableUnclaimedMap, ) { var state = initialState @@ -108,7 +109,7 @@ constructor( */ private suspend fun Flow.handleOneLoadRequest( state: CursorWindow, - pagedCursor: PagedCursor, + pagedCursor: PagedCursor, unclaimedRecords: MutableUnclaimedMap, ): CursorWindow = mapLatest { loadDirection -> @@ -127,7 +128,7 @@ constructor( * [startPosition]. */ private suspend fun readInitialState( - cursor: PagedCursor, + cursor: PagedCursor, startPosition: Int, unclaimedRecords: MutableUnclaimedMap, ): CursorWindow { @@ -138,13 +139,13 @@ constructor( if (!hasMoreLeft) { // First read the initial page; this might claim some unclaimed Uris val page = - cursor.getPageUris(startPageIdx)?.toPage(mutableMapOf(), unclaimedRecords) + cursor.getPageRows(startPageIdx)?.toPage(mutableMapOf(), unclaimedRecords) // Now that unclaimed Uris are up-to-date, add them first. putAllUnclaimedLeft(unclaimedRecords) // Then add the loaded page page?.let(::putAll) } else { - cursor.getPageUris(startPageIdx)?.toPage(this, unclaimedRecords) + cursor.getPageRows(startPageIdx)?.toPage(this, unclaimedRecords) } // Finally, add the remainder of the unclaimed Uris. if (!hasMoreRight) { @@ -162,7 +163,7 @@ constructor( } private suspend fun CursorWindow.loadMoreRight( - cursor: PagedCursor, + cursor: PagedCursor, unclaimedRecords: MutableUnclaimedMap, ): CursorWindow { val pageNum = lastLoadedPageNum + 1 @@ -181,7 +182,7 @@ constructor( } private suspend fun CursorWindow.loadMoreLeft( - cursor: PagedCursor, + cursor: PagedCursor, unclaimedRecords: MutableUnclaimedMap, ): CursorWindow { val pageNum = firstLoadedPageNum - 1 @@ -207,7 +208,7 @@ constructor( private suspend fun readPage( state: CursorWindow, - pagedCursor: PagedCursor, + pagedCursor: PagedCursor, pageNum: Int, unclaimedRecords: MutableUnclaimedMap, ): PreviewMap = @@ -216,30 +217,33 @@ constructor( private suspend fun M.readAndPutPage( state: CursorWindow, - pagedCursor: PagedCursor, + pagedCursor: PagedCursor, pageNum: Int, unclaimedRecords: MutableUnclaimedMap, ): M = pagedCursor - .getPageUris(pageNum) // TODO: what do we do if the load fails? - ?.filter { it !in state.merged } + .getPageRows(pageNum) // TODO: what do we do if the load fails? + ?.filter { it.uri !in state.merged } ?.toPage(this, unclaimedRecords) ?: this - private suspend fun Sequence.toPage( + private suspend fun Sequence.toPage( destination: M, unclaimedRecords: MutableUnclaimedMap, ): M = // Restrict parallelism so as to not overload the metadata reader; anecdotally, too // many parallel queries causes failures. - mapParallel(parallelism = 4) { uri -> createPreviewModel(uri, unclaimedRecords) } + mapParallel(parallelism = 4) { row -> createPreviewModel(row, unclaimedRecords) } .associateByTo(destination) { it.uri } - private fun createPreviewModel(uri: Uri, unclaimedRecords: MutableUnclaimedMap): PreviewModel = - unclaimedRecords.remove(uri)?.second + private fun createPreviewModel( + row: CursorRow, + unclaimedRecords: MutableUnclaimedMap, + ): PreviewModel = + unclaimedRecords.remove(row.uri)?.second ?: PreviewModel( - uri = uri, - mimeType = uriMetadataReader.getMetadata(uri).mimeType, + uri = row.uri, + mimeType = uriMetadataReader.getMetadata(row.uri).mimeType, ) private fun M.putAllUnclaimedRight(unclaimed: UnclaimedMap): M = @@ -275,7 +279,7 @@ private fun M.putAllUnclaimedWhere( .map { it.key to it.value.second } .toMap(this) -private fun PagedCursor.getPageUris(pageNum: Int): Sequence? = +private fun PagedCursor.getPageRows(pageNum: Int): Sequence? = get(pageNum)?.filterNotNull() @Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class PageSize 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 9bc7ae63..927a3a84 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 @@ -21,6 +21,7 @@ import com.android.intentresolver.contentpreview.UriMetadataReader import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.CursorResolver import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggle +import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.CursorRow import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import com.android.intentresolver.inject.ContentUris import com.android.intentresolver.inject.FocusedItemIndex @@ -39,7 +40,7 @@ constructor( @FocusedItemIndex private val focusedItemIdx: Int, @ContentUris private val selectedItems: List<@JvmSuppressWildcards Uri>, private val uriMetadataReader: UriMetadataReader, - @PayloadToggle private val cursorResolver: CursorResolver<@JvmSuppressWildcards Uri?>, + @PayloadToggle private val cursorResolver: CursorResolver<@JvmSuppressWildcards CursorRow?>, ) { suspend fun activate() = coroutineScope { val cursor = async { cursorResolver.getCursor() } 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 new file mode 100644 index 00000000..f1d856ac --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/CursorRow.kt @@ -0,0 +1,23 @@ +/* + * 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.model + +import android.net.Uri +import android.util.Size + +/** Represents additional content cursor row */ +data class CursorRow(val uri: Uri, val previewSize: Size?) -- cgit v1.2.3-59-g8ed1b