Refactored SearchRepository to expose search results through single flow.
- Combined search results and query index to be provided from single flow, as both fields were highly cohesive. This will make easier for collectors to maintain parity.
- Updated prev() and next() implementation to provide a shallow copy of query results while modifying only queryResultIndex.
Test: ./gradlew :pdf:pdf-viewer:connectedDebugAndroidTest
Bug: 379054326
Change-Id: I3279c55fe4511d876748e9a53631acc52472be14
diff --git a/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/SandboxedPdfDocumentTest.kt b/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/SandboxedPdfDocumentTest.kt
index f40d303..dbc7510 100644
--- a/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/SandboxedPdfDocumentTest.kt
+++ b/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/SandboxedPdfDocumentTest.kt
@@ -33,6 +33,7 @@
import junit.framework.TestCase.assertFalse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -151,6 +152,21 @@
}
}
+ @Test
+ fun searchDocument_fullDocumentSearch_withSinglePageResults() = runTest {
+ withDocument(PDF_DOCUMENT) { document ->
+ val query = "pages are all the same size"
+ val pageRange = 0..2
+
+ val results = document.searchDocument(query, pageRange)
+
+ // Assert sparse array doesn't contain empty result lists
+ assertEquals(1, results.size())
+ // Assert single result on first page
+ assertEquals(1, results[0].size)
+ }
+ }
+
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
@Test
fun getSelectionBounds_returnsPageSelection() = runTest {
diff --git a/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt b/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt
index 774bb7a..097d35b 100644
--- a/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt
+++ b/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt
@@ -88,11 +88,14 @@
pageRange: IntRange
): SparseArray<List<PageMatchBounds>> {
return withDocument { document ->
- pageRange
- .map { pageNum ->
- document.searchPageText(pageNum, query).map { it.toContentClass() }
+ SparseArray<List<PageMatchBounds>>(pageRange.last + 1).apply {
+ pageRange.forEach { pageNum ->
+ document
+ .searchPageText(pageNum, query)
+ .takeIf { it.isNotEmpty() }
+ ?.let { put(pageNum, it.map { result -> result.toContentClass() }) }
}
- .toSparseArray(pageRange)
+ }
}
}
diff --git a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/search/SearchRepositoryTest.kt b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/search/SearchRepositoryTest.kt
index 1b39c3ec..4c0c976 100644
--- a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/search/SearchRepositoryTest.kt
+++ b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/search/SearchRepositoryTest.kt
@@ -17,11 +17,9 @@
package androidx.pdf.search
import android.util.SparseArray
-import androidx.core.util.isEmpty
-import androidx.core.util.isNotEmpty
import androidx.pdf.content.PageMatchBounds
-import androidx.pdf.search.model.SearchResults
-import androidx.pdf.search.model.SelectedSearchResult
+import androidx.pdf.search.model.NoQuery
+import androidx.pdf.search.model.QueryResults
import androidx.pdf.view.FakePdfDocument
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -59,38 +57,42 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 5)
+ produceSearchResults(query = "test", currentVisiblePage = 5)
- val results = searchResults.value
+ var results = queryResults.value as QueryResults.Matched
// Assert results exists on 3 pages
- assertEquals(3, results.results.size())
- assertEquals(5, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(3, results.resultBounds.size())
+ assertEquals(5, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result on same page
- assertEquals(5, selectedSearchResult.value?.pageNum)
- assertEquals(1, selectedSearchResult.value?.currentIndex)
+ assertEquals(5, results.queryResultsIndex.pageNum)
+ assertEquals(1, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result on next page
// in forward direction
- assertEquals(10, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(10, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result cyclically
- assertEquals(1, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(1, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch previous result
- prev()
+ producePreviousResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to previous result cyclically
- assertEquals(10, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(10, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
}
}
@@ -101,19 +103,20 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 7)
+ produceSearchResults(query = "test", currentVisiblePage = 7)
- val results = searchResults.value
+ var results = queryResults.value as QueryResults.Matched
// Assert results exists on 3 pages
- assertEquals(3, results.results.size())
- assertEquals(10, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(3, results.resultBounds.size())
+ assertEquals(10, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result cyclically
- assertEquals(1, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(1, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
}
}
@@ -124,20 +127,21 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 11)
+ produceSearchResults(query = "test", currentVisiblePage = 11)
- val results = searchResults.value
+ var results = queryResults.value as QueryResults.Matched
// Assert results exists on 3 pages
- assertEquals(3, results.results.size())
+ assertEquals(3, results.resultBounds.size())
// Assert selectedSearchResult point to next result cyclically
- assertEquals(1, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(1, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result on next page
- assertEquals(5, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(5, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
}
}
@@ -148,46 +152,47 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 11)
+ produceSearchResults(query = "test", currentVisiblePage = 11)
- val results = searchResults.value
+ val results = queryResults.value
// Assert no results returned
- assertEquals(0, results.results.size())
+ assertTrue(results is QueryResults.NoMatch)
+ assertEquals("test", (results as QueryResults.NoMatch).query)
}
}
@Test(expected = NoSuchElementException::class)
- fun testPrevOperation_noMatchingResults() = runTest {
+ fun testFindPrevOperation_noMatchingResults() = runTest {
val fakeResults = createFakeSearchResults()
val fakePdfDocument = FakePdfDocument(searchResults = fakeResults)
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 11)
+ produceSearchResults(query = "test", currentVisiblePage = 11)
- val results = searchResults.value
- assertEquals(0, results.results.size())
+ val results = queryResults.value
+ assertTrue(results is QueryResults.NoMatch)
// fetch previous result, should throw [NoSuchElementException]
- prev()
+ producePreviousResult()
}
}
@Test(expected = NoSuchElementException::class)
- fun testNextOperation_noMatchingResults() = runTest {
+ fun testFindNextOperation_noMatchingResults() = runTest {
val fakeResults = createFakeSearchResults()
val fakePdfDocument = FakePdfDocument(searchResults = fakeResults)
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 10)
+ produceSearchResults(query = "test", currentVisiblePage = 10)
- val results = searchResults.value
- assertEquals(0, results.results.size())
+ val results = queryResults.value
+ assertTrue(results is QueryResults.NoMatch)
// fetch next result, should throw [NoSuchElementException]
- next()
+ produceNextResult()
}
}
@@ -198,67 +203,16 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 11)
+ produceSearchResults(query = "test", currentVisiblePage = 11)
- assertEquals(3, searchResults.value.results.size())
+ val results = queryResults.value as QueryResults.Matched
+ assertEquals(3, results.resultBounds.size())
// clear results
clearSearchResults()
// assert results are cleared
- assertTrue(searchResults.value.results.isEmpty())
- }
- }
-
- @Test
- fun testSettingStateToRepository() = runTest {
- val fakeResults = createFakeSearchResults()
- val fakePdfDocument = FakePdfDocument(searchResults = fakeResults)
- val currentVisiblePage = 11
-
- with(SearchRepository(fakePdfDocument)) {
- // search document
- searchDocument(query = "test", currentVisiblePage = currentVisiblePage)
-
- // assert there are no results
- assertEquals(0, searchResults.value.results.size())
-
- // set results
- setState(
- searchResults = SearchResults("test", createFakeSearchResults(1, 5, 5, 10)),
- selectedSearchResult = SelectedSearchResult(5, 1),
- currentVisiblePage = currentVisiblePage
- )
-
- // assert results are set
- assertTrue(searchResults.value.results.isNotEmpty())
- assertEquals(5, selectedSearchResult.value?.pageNum)
- assertEquals(1, selectedSearchResult.value?.currentIndex)
- }
- }
-
- @Test(expected = NoSuchElementException::class)
- fun testSettingEmptyResultsToRepository() = runTest {
- val fakeResults = createFakeSearchResults()
- val fakePdfDocument = FakePdfDocument(searchResults = fakeResults)
- val currentVisiblePage = 11
-
- with(SearchRepository(fakePdfDocument)) {
- // search document
- searchDocument(query = "test", currentVisiblePage = currentVisiblePage)
-
- // assert there are no results
- assertEquals(0, searchResults.value.results.size())
-
- // set results
- setState(
- searchResults = SearchResults("test", SparseArray()),
- selectedSearchResult = null,
- currentVisiblePage = currentVisiblePage
- )
-
- // fetch next result, should throw [NoSuchElementException]
- next()
+ assertTrue(queryResults.value is NoQuery)
}
}
}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/CyclicSparseArrayIterator.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/CyclicSparseArrayIterator.kt
index 8b7895a..8a16283 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/CyclicSparseArrayIterator.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/CyclicSparseArrayIterator.kt
@@ -19,7 +19,7 @@
import android.util.SparseArray
import androidx.annotation.RestrictTo
import androidx.pdf.content.PageMatchBounds
-import androidx.pdf.search.model.SelectedSearchResult
+import androidx.pdf.search.model.QueryResultsIndex
/**
* A cyclic iterator implementation over SparseArray.
@@ -54,13 +54,13 @@
}
/** Get the current state of selected search result. */
- fun current(): SelectedSearchResult {
+ fun current(): QueryResultsIndex {
val currentPageNum = pageNumList[pageNumIndex]
- return SelectedSearchResult(currentPageNum, searchIndexOnPage)
+ return QueryResultsIndex(pageNum = currentPageNum, resultBoundsIndex = searchIndexOnPage)
}
/** Move to the nex element in the current page, or to the next page cyclically. */
- fun next(): SelectedSearchResult {
+ fun next(): QueryResultsIndex {
if (totalPages == 0) {
throw NoSuchElementException("No elements to iterate.")
}
@@ -80,7 +80,7 @@
}
/** Move to the previous element in the page list, or to the previous page cyclically. */
- fun prev(): SelectedSearchResult {
+ fun prev(): QueryResultsIndex {
if (totalPages == 0) {
throw NoSuchElementException("No elements to iterate.")
}
@@ -94,6 +94,8 @@
// If we're at the beginning of the current page, move to the previous page
if (searchIndexOnPage == resultsOnPage.size - 1) {
pageNumIndex = (pageNumIndex - 1 + totalPages) % totalPages
+ // update the search index of page to last result on updated page
+ searchIndexOnPage = searchData.valueAt(pageNumIndex).lastIndex
}
return current()
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/SearchRepository.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/SearchRepository.kt
index 80cce7e..3de7dbb 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/SearchRepository.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/SearchRepository.kt
@@ -17,11 +17,11 @@
package androidx.pdf.search
import androidx.annotation.RestrictTo
-import androidx.core.util.isEmpty
import androidx.core.util.isNotEmpty
import androidx.pdf.PdfDocument
-import androidx.pdf.search.model.SearchResults
-import androidx.pdf.search.model.SelectedSearchResult
+import androidx.pdf.search.model.NoQuery
+import androidx.pdf.search.model.QueryResults
+import androidx.pdf.search.model.SearchResultState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -46,23 +46,17 @@
* to Dispatcher.IO.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class SearchRepository(
+public class SearchRepository(
private val pdfDocument: PdfDocument,
+ // TODO(b/384001800) Remove dispatcher
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
- private val _searchResults: MutableStateFlow<SearchResults> = MutableStateFlow(SearchResults())
+ private val _queryResults: MutableStateFlow<SearchResultState> = MutableStateFlow(NoQuery)
/** Stream of search results for a given query. */
- val searchResults: StateFlow<SearchResults>
- get() = _searchResults.asStateFlow()
-
- private val _selectedSearchResult: MutableStateFlow<SelectedSearchResult?> =
- MutableStateFlow(null)
-
- /** Stream of selected search results. */
- val selectedSearchResult: StateFlow<SelectedSearchResult?>
- get() = _selectedSearchResult.asStateFlow()
+ public val queryResults: StateFlow<SearchResultState>
+ get() = _queryResults.asStateFlow()
private lateinit var cyclicIterator: CyclicSparseArrayIterator
@@ -71,93 +65,108 @@
*
* @param query: The search query string.
* @param currentVisiblePage: Provides current visible document page, which is required to
- * search from specific page and to calculate initial [selectedSearchResult]
+ * search from specific page and to calculate initial QueryResultsIndex.
*
- * Results would be updated to [searchResults] in the coroutine collecting the flow.
+ * Results would be updated to [queryResults] in the coroutine collecting the flow.
*/
- suspend fun searchDocument(query: String, currentVisiblePage: Int) {
- if (query.isEmpty()) return
+ public suspend fun produceSearchResults(query: String, currentVisiblePage: Int) {
+ if (query.isBlank()) {
+ clearSearchResults()
+ return
+ }
- // Clear the existing results
- clearSearchResults()
+ val searchPageRange = IntRange(start = 0, endInclusive = pdfDocument.pageCount - 1)
// search should be a background work, move execution on to provided [dispatcher]
// to make [searchDocument] main-safe
- val currentResult =
+ val searchResults =
withContext(dispatcher) {
- SearchResults(
- searchQuery = query,
- results =
- pdfDocument.searchDocument(
- query = query,
- pageRange = IntRange(start = 0, endInclusive = pdfDocument.pageCount)
- )
- )
+ pdfDocument.searchDocument(query = query, pageRange = searchPageRange)
}
- // update results
- _searchResults.update { currentResult }
+ val queryResults =
+ if (searchResults.isNotEmpty()) {
+ /*
+ When search results are available for a query, we initialize a cyclic iterator.
+ This iterator is used to traverse the results when `findPrev()` and `findNext()` are called.
+ */
+ cyclicIterator = CyclicSparseArrayIterator(searchResults, currentVisiblePage)
- if (currentResult.results.isNotEmpty()) {
- // Init cyclic iterator
- cyclicIterator = CyclicSparseArrayIterator(currentResult.results, currentVisiblePage)
+ QueryResults.Matched(
+ query = query,
+ pageRange = searchPageRange,
+ resultBounds = searchResults,
+ /* Set [queryResultsIndex] to cyclicIterator.current() which points to first result
+ on or nearest page to currentVisiblePage in forward direction. */
+ queryResultsIndex = cyclicIterator.current()
+ )
+ } else {
+ QueryResults.NoMatch(query = query, pageRange = searchPageRange)
+ }
- // update initial selection
- _selectedSearchResult.update { cyclicIterator.current() }
- }
+ _queryResults.update { queryResults }
}
/**
* Iterate through searchResults in backward direction.
*
- * Results would be updated to [selectedSearchResult] in the coroutine collecting the flow.
+ * Results would be updated to [queryResults] in the coroutine collecting the flow.
*
* Throws [NoSuchElementException] is search results are empty.
*/
- suspend fun prev() {
- if (searchResults.value.results.isEmpty())
+ public suspend fun producePreviousResult() {
+ val currentResult = queryResults.value
+
+ if (currentResult !is QueryResults.Matched)
throw NoSuchElementException("Iteration not possible over empty results")
- _selectedSearchResult.update { cyclicIterator.prev() }
+ /*
+ Create a shallow copy of the query result, updating only the `queryResultIndex`
+ to point to the previous element in the `resultsBounds` of the current query result.
+ */
+ val prevResult =
+ QueryResults.Matched(
+ query = currentResult.query,
+ resultBounds = currentResult.resultBounds,
+ pageRange = currentResult.pageRange,
+ queryResultsIndex = cyclicIterator.prev()
+ )
+
+ _queryResults.update { prevResult }
}
/**
* Iterate through searchResults in forward direction.
*
- * Results would be updated to [selectedSearchResult] in the coroutine collecting the flow.
+ * Results would be updated to [queryResults] in the coroutine collecting the flow.
*
* Throws [NoSuchElementException] is search results are empty.
*/
- suspend fun next() {
- if (searchResults.value.results.isEmpty())
+ public suspend fun produceNextResult() {
+ val currentResult = queryResults.value
+
+ if (currentResult !is QueryResults.Matched)
throw NoSuchElementException("Iteration not possible over empty results")
- _selectedSearchResult.update { cyclicIterator.next() }
+ /*
+ Create a shallow copy of the query result, updating only the `queryResultIndex`
+ to point to the next element in the `resultsBounds` of the current query result.
+ */
+ val nextResult =
+ QueryResults.Matched(
+ query = currentResult.query,
+ resultBounds = currentResult.resultBounds,
+ pageRange = currentResult.pageRange,
+ queryResultsIndex = cyclicIterator.next()
+ )
+
+ _queryResults.update { nextResult }
}
/**
- * Resets [searchResults] and [selectedSearchResult] to initial state. This would be required to
- * handle close/cancel action.
+ * Resets [queryResults] to initial state. This would be required to handle close/cancel action.
*/
- fun clearSearchResults() {
- _searchResults.update { SearchResults() }
- _selectedSearchResult.update { null }
- }
-
- /**
- * Set [searchResults] and [selectedSearchResult] flows to provided value.
- *
- * This should be utilized when result state is already available(muck like in restore scenario)
- */
- fun setState(
- searchResults: SearchResults,
- selectedSearchResult: SelectedSearchResult?,
- currentVisiblePage: Int
- ) {
- _searchResults.update { searchResults }
- _selectedSearchResult.update { selectedSearchResult }
- // initiate iterator is results are not empty
- if (searchResults.results.isNotEmpty())
- cyclicIterator = CyclicSparseArrayIterator(searchResults.results, currentVisiblePage)
+ public fun clearSearchResults() {
+ _queryResults.update { NoQuery }
}
}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResults.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResults.kt
new file mode 100644
index 0000000..52c2f36
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResults.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 androidx.pdf.search.model
+
+import android.util.SparseArray
+import androidx.annotation.RestrictTo
+import androidx.pdf.content.PageMatchBounds
+
+/** A sealed interface that encapsulates the various states of a search operation's result. */
+@RestrictTo(RestrictTo.Scope.LIBRARY) public sealed interface SearchResultState
+
+/**
+ * Represents the initial state when no query has been submitted to trigger a search operation. This
+ * state occurs before any search is initiated.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY) public object NoQuery : SearchResultState
+
+/**
+ * A sealed class representing the outcome of a search operation.
+ *
+ * @param query The search query that initiated the search.
+ * @param pageRange The range of PDF pages involved in the search.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public sealed class QueryResults(public val query: String, public val pageRange: IntRange) :
+ SearchResultState {
+
+ /**
+ * Represents the state when no results are found after a search operation. This indicates that
+ * the search yielded no matching results.
+ *
+ * @param query The search query that was executed.
+ * @param pageRange The range of PDF pages included in the search.
+ */
+ public class NoMatch(query: String, pageRange: IntRange) : QueryResults(query, pageRange)
+
+ /**
+ * Represents the state when a search operation returns results.
+ *
+ * @param query The search query that was executed.
+ * @param pageRange The range of PDF pages included in the search.
+ * @param resultBounds A mapping of match bounds for the results, indexed by their position.
+ * @param queryResultsIndex Represents an index pointer to an element in [resultBounds].
+ */
+ public class Matched(
+ query: String,
+ pageRange: IntRange,
+ public val resultBounds: SparseArray<List<PageMatchBounds>>,
+ public val queryResultsIndex: QueryResultsIndex,
+ ) : QueryResults(query, pageRange)
+}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SelectedSearchResult.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResultsIndex.kt
similarity index 66%
rename from pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SelectedSearchResult.kt
rename to pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResultsIndex.kt
index cb39cb7..36aa484 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SelectedSearchResult.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResultsIndex.kt
@@ -18,13 +18,13 @@
import androidx.annotation.RestrictTo
-/** Model class to hold current selected search result. */
+/** A model class that holds the index of a data element within [QueryResults]'s resultBounds. */
@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class SelectedSearchResult(
+public class QueryResultsIndex(
- /** Represents document page number where current search result is selected */
- val pageNum: Int,
+ /** The page number of the document where the current search result is located. */
+ public val pageNum: Int,
- /** index of result on the page specified by [pageNum] */
- val currentIndex: Int
+ /** The index of the search result on the page specified by [pageNum]. */
+ public val resultBoundsIndex: Int
)
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SearchResults.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SearchResults.kt
deleted file mode 100644
index 407cd13..0000000
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SearchResults.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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 androidx.pdf.search.model
-
-import android.util.SparseArray
-import androidx.annotation.RestrictTo
-import androidx.pdf.content.PageMatchBounds
-
-/** Model class to hold search results over pdf document for a search query. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class SearchResults(
- /**
- * search query provided to initiate search
- *
- * By default it will be empty string.
- */
- val searchQuery: String = "",
- /**
- * search results in pdf document for [searchQuery]
- *
- * By default it will be initialized to empty [SparseArray].
- */
- val results: SparseArray<List<PageMatchBounds>> = SparseArray()
-)