Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem

import android.content.ContentResolver
import android.database.Cursor
import android.provider.MediaStore
import com.amaze.filemanager.filesystem.RootHelper
Expand All @@ -31,9 +32,25 @@ class IndexedSearch(
query: String,
path: String,
searchParameters: SearchParameters,
private val cursor: Cursor,
private val contentResolver: ContentResolver,
) : FileSearch(query, path, searchParameters) {
override suspend fun search(filter: SearchFilter) {
val projection =
arrayOf(
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.DISPLAY_NAME,
)
contentResolver
.query(MediaStore.Files.getContentUri("external"), projection, null, null, null)
?.use { cursor ->
searchCursor(cursor, filter)
}
}

private suspend fun searchCursor(
cursor: Cursor,
filter: SearchFilter,
) {
if (cursor.count > 0 && cursor.moveToFirst()) {
do {
val nextPath =
Expand All @@ -59,7 +76,5 @@ class IndexedSearch(
}
} while (cursor.moveToNext() && coroutineContext.isActive)
}

cursor.close()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ package com.amaze.filemanager.ui.activities

import android.app.Application
import android.content.Intent
import android.provider.MediaStore
import androidx.collection.LruCache
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
Expand Down Expand Up @@ -138,22 +137,17 @@ class MainActivityViewModel(val applicationContext: Application) :
mainActivity: MainActivity,
query: String,
): LiveData<List<SearchResult>> {
val projection =
arrayOf(
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.DISPLAY_NAME,
)
val cursor =
mainActivity
.contentResolver
.query(MediaStore.Files.getContentUri("external"), projection, null, null, null)
?: return MutableLiveData()

val searchParameters = createSearchParameters(mainActivity)

val path = mainActivity.currentMainFragment?.currentPath ?: ""

val indexedSearch = IndexedSearch(query, path, searchParameters, cursor)
val indexedSearch =
IndexedSearch(
query,
path,
searchParameters,
applicationContext.contentResolver,
)

lastSearchJob =
viewModelScope.launch(Dispatchers.IO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@

package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem

import android.content.ContentResolver
import android.database.Cursor
import android.provider.MediaStore
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.just
import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert
Expand All @@ -36,6 +40,10 @@ import org.junit.Test
import java.util.EnumSet

class IndexedSearchTest {
companion object {
private const val EXTERNAL_VOLUME = "external"
}

@get:Rule
val rule = InstantTaskExecutorRule()

Expand All @@ -45,6 +53,9 @@ class IndexedSearchTest {
@RelaxedMockK
lateinit var mockCursor: Cursor

@RelaxedMockK
lateinit var contentResolver: ContentResolver

val filePath = "/test/abc.txt"
val fileName = "abc.txt"

Expand All @@ -64,6 +75,16 @@ class IndexedSearchTest {

every { mockCursor.getString(dataColumn) } returns filePath
every { mockCursor.getString(displayNameColumn) } returns fileName
every { mockCursor.close() } just runs
every {
contentResolver.query(
MediaStore.Files.getContentUri(EXTERNAL_VOLUME),
any(),
null,
null,
null,
)
} returns mockCursor
}

/** Clean up all mocks */
Expand All @@ -87,12 +108,12 @@ class IndexedSearchTest {
"ab",
"/",
EnumSet.noneOf(SearchParameter::class.java),
mockCursor,
contentResolver,
)
indexedSearch.foundFilesLiveData.observeForever { actualResults ->
Assert.assertNotNull(actualResults)
Assert.assertEquals(expectedNames, actualResults!!.map { (file, _) -> file.name })
Assert.assertEquals(expectedPaths, actualResults!!.map { (file, _) -> file.path })
Assert.assertEquals(expectedNames, actualResults.map { (file, _) -> file.name })
Assert.assertEquals(expectedPaths, actualResults.map { (file, _) -> file.path })
Assert.assertEquals(expectedRanges, actualResults.map { it.matchRange })
}
runTest {
Expand All @@ -111,7 +132,7 @@ class IndexedSearchTest {
"ba",
"/",
EnumSet.noneOf(SearchParameter::class.java),
mockCursor,
contentResolver,
)
indexedSearch.foundFilesLiveData.observeForever { actualResults ->
Assert.assertNotNull(actualResults)
Expand All @@ -136,7 +157,7 @@ class IndexedSearchTest {
"te",
"/",
EnumSet.noneOf(SearchParameter::class.java),
mockCursor,
contentResolver,
)
indexedSearch.foundFilesLiveData.observeForever { actualResults ->
Assert.assertNotNull(actualResults)
Expand All @@ -150,5 +171,82 @@ class IndexedSearchTest {
}
}

/**
* The MediaStore query should happen as part of [IndexedSearch.search], so callers can decide
* the coroutine dispatcher before any provider work starts.
*/
@Test
fun testContentResolverQueryRunsOnlyWhenSearchStarts() {
val indexedSearch =
IndexedSearch(
"ab",
"/",
EnumSet.noneOf(SearchParameter::class.java),
contentResolver,
)

verify(exactly = 0) {
contentResolver.query(
MediaStore.Files.getContentUri(EXTERNAL_VOLUME),
any(),
null,
null,
null,
)
}

runTest {
indexedSearch.search()
}

verify(exactly = 1) {
contentResolver.query(
MediaStore.Files.getContentUri(EXTERNAL_VOLUME),
any(),
null,
null,
null,
)
}
}

/**
* If MediaStore cannot provide a cursor, indexed search should return without crashing.
*/
@Test
fun testNullCursorDoesNotCrash() {
every {
contentResolver.query(
MediaStore.Files.getContentUri(EXTERNAL_VOLUME),
any(),
null,
null,
null,
)
} returns null

val indexedSearch =
IndexedSearch(
"ab",
"/",
EnumSet.noneOf(SearchParameter::class.java),
contentResolver,
)

runTest {
indexedSearch.search()
}

verify(exactly = 1) {
contentResolver.query(
MediaStore.Files.getContentUri(EXTERNAL_VOLUME),
any(),
null,
null,
null,
)
}
}

private fun listNotEmptyError(size: Int) = "List was not empty as expected but had $size elements"
}