adding test for fetching media folders
This commit is contained in:
parent
dcf4b4c80a
commit
42761c0899
|
@ -13,11 +13,18 @@ data class ContentResolverQuery(
|
|||
)
|
||||
|
||||
inline fun <T> ContentResolver.reduce(query: ContentResolverQuery, operation: (Cursor) -> T): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
return this.reduce(query, mutableListOf<T>()) { acc, cursor ->
|
||||
acc.add(operation(cursor))
|
||||
acc
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> ContentResolver.reduce(query: ContentResolverQuery, initial: T, operation: (T, Cursor) -> T): T {
|
||||
var accumulator: T = initial
|
||||
this.query(query.uri, query.projection.toTypedArray(), query.selection, query.selectionArgs.toTypedArray(), query.sortBy).use { cursor ->
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
result.add(operation(cursor))
|
||||
accumulator = operation(accumulator, cursor)
|
||||
}
|
||||
}
|
||||
return result
|
||||
return accumulator
|
||||
}
|
||||
|
|
|
@ -1,37 +1,39 @@
|
|||
package app.dapk.st.messenger.gallery
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore.Images
|
||||
import app.dapk.st.core.ContentResolverQuery
|
||||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.reduce
|
||||
import app.dapk.st.core.withIoContext
|
||||
|
||||
class FetchMediaFoldersUseCase(
|
||||
private val contentResolver: ContentResolver,
|
||||
private val uriAvoidance: MediaUriAvoidance,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
|
||||
suspend fun fetchFolders(): List<Folder> {
|
||||
return dispatchers.withIoContext {
|
||||
val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED)
|
||||
val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?"
|
||||
val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC"
|
||||
val query = ContentResolverQuery(
|
||||
uriAvoidance.externalContentUri,
|
||||
listOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED),
|
||||
"${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?",
|
||||
listOf("%image/svg%"),
|
||||
"${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC"
|
||||
)
|
||||
|
||||
val folders = mutableMapOf<String, Folder>()
|
||||
val contentUri = Images.Media.EXTERNAL_CONTENT_URI
|
||||
contentResolver.query(contentUri, projection, selection, arrayOf("%image/svg%"), sortBy).use { cursor ->
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
|
||||
val thumbnail = ContentUris.withAppendedId(contentUri, rowId)
|
||||
val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1]))
|
||||
val title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2])) ?: ""
|
||||
val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(projection[3]))
|
||||
val folder = folders.getOrPut(bucketId) { Folder(bucketId, title, thumbnail) }
|
||||
folder.incrementItemCount()
|
||||
}
|
||||
}
|
||||
folders.values.toList()
|
||||
contentResolver.reduce(query, mutableMapOf<String, Folder>()) { acc, cursor ->
|
||||
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media._ID))
|
||||
val thumbnail = uriAvoidance.uriAppender(query.uri, rowId)
|
||||
val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.BUCKET_ID))
|
||||
val title = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.BUCKET_DISPLAY_NAME)) ?: ""
|
||||
val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED))
|
||||
val folder = acc.getOrPut(bucketId) { Folder(bucketId, title, thumbnail) }
|
||||
folder.incrementItemCount()
|
||||
acc
|
||||
}.values.toList()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import app.dapk.st.core.withIoContext
|
|||
|
||||
class FetchMediaUseCase(
|
||||
private val contentResolver: ContentResolver,
|
||||
private val uriAvoidance: UriAvoidance,
|
||||
private val uriAvoidance: MediaUriAvoidance,
|
||||
private val dispatchers: CoroutineDispatchers
|
||||
) {
|
||||
|
||||
|
@ -54,11 +54,6 @@ class FetchMediaUseCase(
|
|||
|
||||
private fun getHeightColumn(orientation: Int) =
|
||||
if (orientation == 0 || orientation == 180) MediaStore.Images.Media.HEIGHT else MediaStore.Images.Media.WIDTH
|
||||
|
||||
class UriAvoidance(
|
||||
val uriAppender: (Uri, Long) -> Uri,
|
||||
val externalContentUri: Uri,
|
||||
)
|
||||
}
|
||||
|
||||
data class Media(
|
||||
|
|
|
@ -7,7 +7,6 @@ import app.dapk.st.core.CoroutineDispatchers
|
|||
import app.dapk.st.core.JobBag
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.createStateViewModel
|
||||
import app.dapk.st.messenger.gallery.FetchMediaUseCase.UriAvoidance
|
||||
import app.dapk.st.messenger.gallery.state.ImageGalleryState
|
||||
import app.dapk.st.messenger.gallery.state.imageGalleryReducer
|
||||
|
||||
|
@ -17,17 +16,14 @@ class ImageGalleryModule(
|
|||
) : ProvidableModule {
|
||||
|
||||
fun imageGalleryState(roomName: String): ImageGalleryState = createStateViewModel {
|
||||
val uriAvoidance = MediaUriAvoidance(
|
||||
uriAppender = { uri, rowId -> ContentUris.withAppendedId(uri, rowId) },
|
||||
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
)
|
||||
imageGalleryReducer(
|
||||
roomName = roomName,
|
||||
FetchMediaFoldersUseCase(contentResolver, dispatchers),
|
||||
FetchMediaUseCase(
|
||||
contentResolver,
|
||||
UriAvoidance(
|
||||
uriAppender = { uri, rowId -> ContentUris.withAppendedId(uri, rowId) },
|
||||
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
),
|
||||
dispatchers,
|
||||
),
|
||||
FetchMediaFoldersUseCase(contentResolver, uriAvoidance, dispatchers),
|
||||
FetchMediaUseCase(contentResolver, uriAvoidance, dispatchers),
|
||||
JobBag(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package app.dapk.st.messenger.gallery
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
class MediaUriAvoidance(
|
||||
val uriAppender: (Uri, Long) -> Uri,
|
||||
val externalContentUri: Uri,
|
||||
)
|
|
@ -0,0 +1,91 @@
|
|||
package app.dapk.st.messenger.gallery
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import fake.CreateCursorScope
|
||||
import fake.FakeContentResolver
|
||||
import fake.FakeUri
|
||||
import fake.createCursor
|
||||
import fixture.CoroutineDispatchersFixture.aCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
|
||||
private val A_EXTERNAL_CONTENT_URI = FakeUri()
|
||||
private const val A_BUCKET_ID = "a-bucket-id"
|
||||
private const val A_SECOND_BUCKET_ID = "another-bucket"
|
||||
private const val A_DISPLAY_NAME = "a-bucket-name"
|
||||
private const val A_DATE_MODIFIED = 5000L
|
||||
|
||||
class FetchMediaFoldersUseCaseTest {
|
||||
|
||||
private val fakeContentResolver = FakeContentResolver()
|
||||
private val fakeUriAppender = FakeUriAppender()
|
||||
private val uriAvoidance = MediaUriAvoidance(
|
||||
uriAppender = fakeUriAppender,
|
||||
externalContentUri = A_EXTERNAL_CONTENT_URI.instance,
|
||||
)
|
||||
|
||||
private val useCase = FetchMediaFoldersUseCase(fakeContentResolver.instance, uriAvoidance, aCoroutineDispatchers())
|
||||
|
||||
@Test
|
||||
fun `given cursor content, when get folder, then reads unique folders`() = runTest {
|
||||
fakeContentResolver.givenFolderQuery().returns(createCursor {
|
||||
addFolderRow(rowId = 1, A_BUCKET_ID)
|
||||
addFolderRow(rowId = 2, A_BUCKET_ID)
|
||||
addFolderRow(rowId = 3, A_SECOND_BUCKET_ID)
|
||||
})
|
||||
|
||||
val result = useCase.fetchFolders()
|
||||
|
||||
result shouldBeEqualTo listOf(
|
||||
Folder(
|
||||
bucketId = A_BUCKET_ID,
|
||||
title = A_DISPLAY_NAME,
|
||||
thumbnail = fakeUriAppender.get(rowId = 1),
|
||||
),
|
||||
Folder(
|
||||
bucketId = A_SECOND_BUCKET_ID,
|
||||
title = A_DISPLAY_NAME,
|
||||
thumbnail = fakeUriAppender.get(rowId = 3),
|
||||
),
|
||||
)
|
||||
result[0].itemCount shouldBeEqualTo 2
|
||||
result[1].itemCount shouldBeEqualTo 1
|
||||
}
|
||||
|
||||
private fun CreateCursorScope.addFolderRow(rowId: Long, bucketId: String) {
|
||||
addRow(
|
||||
MediaStore.Images.Media._ID to rowId,
|
||||
MediaStore.Images.Media.BUCKET_ID to bucketId,
|
||||
MediaStore.Images.Media.BUCKET_DISPLAY_NAME to A_DISPLAY_NAME,
|
||||
MediaStore.Images.Media.DATE_MODIFIED to A_DATE_MODIFIED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun FakeContentResolver.givenFolderQuery() = this.givenQueryResult(
|
||||
A_EXTERNAL_CONTENT_URI.instance,
|
||||
arrayOf(
|
||||
MediaStore.Images.Media._ID,
|
||||
MediaStore.Images.Media.BUCKET_ID,
|
||||
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
|
||||
MediaStore.Images.Media.DATE_MODIFIED
|
||||
),
|
||||
"${isNotPending()} AND ${MediaStore.Images.Media.BUCKET_ID} AND ${MediaStore.Images.Media.MIME_TYPE} NOT LIKE ?",
|
||||
arrayOf("%image/svg%"),
|
||||
"${MediaStore.Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${MediaStore.Images.Media.DATE_MODIFIED} DESC",
|
||||
)
|
||||
|
||||
class FakeUriAppender : (Uri, Long) -> Uri {
|
||||
|
||||
private val uris = mutableMapOf<Long, FakeUri>()
|
||||
|
||||
override fun invoke(uri: Uri, rowId: Long): Uri {
|
||||
val fakeUri = FakeUri()
|
||||
uris[rowId] = fakeUri
|
||||
return fakeUri.instance
|
||||
}
|
||||
|
||||
fun get(rowId: Long) = uris[rowId]!!.instance
|
||||
}
|
|
@ -10,6 +10,7 @@ import org.amshove.kluent.shouldBeEqualTo
|
|||
import org.junit.Test
|
||||
|
||||
private val A_EXTERNAL_CONTENT_URI = FakeUri()
|
||||
private val ROW_URI = FakeUri()
|
||||
private const val A_BUCKET_ID = "a-bucket-id"
|
||||
private const val A_ROW_ID = 20L
|
||||
private const val A_MIME_TYPE = "image/png"
|
||||
|
@ -23,9 +24,8 @@ private const val A_HEIGHT = 750
|
|||
class FetchMediaUseCaseTest {
|
||||
|
||||
private val fakeContentResolver = FakeContentResolver()
|
||||
private val appendedUri = FakeUri()
|
||||
private val uriAvoidance = FetchMediaUseCase.UriAvoidance(
|
||||
uriAppender = { _, _ -> appendedUri.instance },
|
||||
private val uriAvoidance = MediaUriAvoidance(
|
||||
uriAppender = { _, _ -> ROW_URI.instance },
|
||||
externalContentUri = A_EXTERNAL_CONTENT_URI.instance,
|
||||
)
|
||||
|
||||
|
@ -50,7 +50,7 @@ class FetchMediaUseCaseTest {
|
|||
result shouldBeEqualTo listOf(
|
||||
Media(
|
||||
id = A_ROW_ID,
|
||||
uri = appendedUri.instance,
|
||||
uri = ROW_URI.instance,
|
||||
mimeType = A_MIME_TYPE,
|
||||
width = A_WIDTH,
|
||||
height = A_HEIGHT,
|
||||
|
@ -79,7 +79,7 @@ class FetchMediaUseCaseTest {
|
|||
result shouldBeEqualTo listOf(
|
||||
Media(
|
||||
id = A_ROW_ID,
|
||||
uri = appendedUri.instance,
|
||||
uri = ROW_URI.instance,
|
||||
mimeType = A_MIME_TYPE,
|
||||
width = A_HEIGHT,
|
||||
height = A_WIDTH,
|
||||
|
|
Loading…
Reference in New Issue