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> {
|
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 ->
|
this.query(query.uri, query.projection.toTypedArray(), query.selection, query.selectionArgs.toTypedArray(), query.sortBy).use { cursor ->
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
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
|
package app.dapk.st.messenger.gallery
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.ContentUris
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore.Images
|
import android.provider.MediaStore.Images
|
||||||
|
import app.dapk.st.core.ContentResolverQuery
|
||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import app.dapk.st.core.reduce
|
||||||
import app.dapk.st.core.withIoContext
|
import app.dapk.st.core.withIoContext
|
||||||
|
|
||||||
class FetchMediaFoldersUseCase(
|
class FetchMediaFoldersUseCase(
|
||||||
private val contentResolver: ContentResolver,
|
private val contentResolver: ContentResolver,
|
||||||
|
private val uriAvoidance: MediaUriAvoidance,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun fetchFolders(): List<Folder> {
|
suspend fun fetchFolders(): List<Folder> {
|
||||||
return dispatchers.withIoContext {
|
return dispatchers.withIoContext {
|
||||||
val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED)
|
val query = ContentResolverQuery(
|
||||||
val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?"
|
uriAvoidance.externalContentUri,
|
||||||
val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC"
|
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>()
|
contentResolver.reduce(query, mutableMapOf<String, Folder>()) { acc, cursor ->
|
||||||
val contentUri = Images.Media.EXTERNAL_CONTENT_URI
|
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media._ID))
|
||||||
contentResolver.query(contentUri, projection, selection, arrayOf("%image/svg%"), sortBy).use { cursor ->
|
val thumbnail = uriAvoidance.uriAppender(query.uri, rowId)
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.BUCKET_ID))
|
||||||
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
|
val title = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.BUCKET_DISPLAY_NAME)) ?: ""
|
||||||
val thumbnail = ContentUris.withAppendedId(contentUri, rowId)
|
val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED))
|
||||||
val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1]))
|
val folder = acc.getOrPut(bucketId) { Folder(bucketId, title, thumbnail) }
|
||||||
val title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2])) ?: ""
|
folder.incrementItemCount()
|
||||||
val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(projection[3]))
|
acc
|
||||||
val folder = folders.getOrPut(bucketId) { Folder(bucketId, title, thumbnail) }
|
}.values.toList()
|
||||||
folder.incrementItemCount()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
folders.values.toList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import app.dapk.st.core.withIoContext
|
||||||
|
|
||||||
class FetchMediaUseCase(
|
class FetchMediaUseCase(
|
||||||
private val contentResolver: ContentResolver,
|
private val contentResolver: ContentResolver,
|
||||||
private val uriAvoidance: UriAvoidance,
|
private val uriAvoidance: MediaUriAvoidance,
|
||||||
private val dispatchers: CoroutineDispatchers
|
private val dispatchers: CoroutineDispatchers
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -54,11 +54,6 @@ class FetchMediaUseCase(
|
||||||
|
|
||||||
private fun getHeightColumn(orientation: Int) =
|
private fun getHeightColumn(orientation: Int) =
|
||||||
if (orientation == 0 || orientation == 180) MediaStore.Images.Media.HEIGHT else MediaStore.Images.Media.WIDTH
|
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(
|
data class Media(
|
||||||
|
|
|
@ -7,7 +7,6 @@ import app.dapk.st.core.CoroutineDispatchers
|
||||||
import app.dapk.st.core.JobBag
|
import app.dapk.st.core.JobBag
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.core.createStateViewModel
|
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.ImageGalleryState
|
||||||
import app.dapk.st.messenger.gallery.state.imageGalleryReducer
|
import app.dapk.st.messenger.gallery.state.imageGalleryReducer
|
||||||
|
|
||||||
|
@ -17,17 +16,14 @@ class ImageGalleryModule(
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun imageGalleryState(roomName: String): ImageGalleryState = createStateViewModel {
|
fun imageGalleryState(roomName: String): ImageGalleryState = createStateViewModel {
|
||||||
|
val uriAvoidance = MediaUriAvoidance(
|
||||||
|
uriAppender = { uri, rowId -> ContentUris.withAppendedId(uri, rowId) },
|
||||||
|
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
|
)
|
||||||
imageGalleryReducer(
|
imageGalleryReducer(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
FetchMediaFoldersUseCase(contentResolver, dispatchers),
|
FetchMediaFoldersUseCase(contentResolver, uriAvoidance, dispatchers),
|
||||||
FetchMediaUseCase(
|
FetchMediaUseCase(contentResolver, uriAvoidance, dispatchers),
|
||||||
contentResolver,
|
|
||||||
UriAvoidance(
|
|
||||||
uriAppender = { uri, rowId -> ContentUris.withAppendedId(uri, rowId) },
|
|
||||||
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
|
||||||
),
|
|
||||||
dispatchers,
|
|
||||||
),
|
|
||||||
JobBag(),
|
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
|
import org.junit.Test
|
||||||
|
|
||||||
private val A_EXTERNAL_CONTENT_URI = FakeUri()
|
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_BUCKET_ID = "a-bucket-id"
|
||||||
private const val A_ROW_ID = 20L
|
private const val A_ROW_ID = 20L
|
||||||
private const val A_MIME_TYPE = "image/png"
|
private const val A_MIME_TYPE = "image/png"
|
||||||
|
@ -23,9 +24,8 @@ private const val A_HEIGHT = 750
|
||||||
class FetchMediaUseCaseTest {
|
class FetchMediaUseCaseTest {
|
||||||
|
|
||||||
private val fakeContentResolver = FakeContentResolver()
|
private val fakeContentResolver = FakeContentResolver()
|
||||||
private val appendedUri = FakeUri()
|
private val uriAvoidance = MediaUriAvoidance(
|
||||||
private val uriAvoidance = FetchMediaUseCase.UriAvoidance(
|
uriAppender = { _, _ -> ROW_URI.instance },
|
||||||
uriAppender = { _, _ -> appendedUri.instance },
|
|
||||||
externalContentUri = A_EXTERNAL_CONTENT_URI.instance,
|
externalContentUri = A_EXTERNAL_CONTENT_URI.instance,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ class FetchMediaUseCaseTest {
|
||||||
result shouldBeEqualTo listOf(
|
result shouldBeEqualTo listOf(
|
||||||
Media(
|
Media(
|
||||||
id = A_ROW_ID,
|
id = A_ROW_ID,
|
||||||
uri = appendedUri.instance,
|
uri = ROW_URI.instance,
|
||||||
mimeType = A_MIME_TYPE,
|
mimeType = A_MIME_TYPE,
|
||||||
width = A_WIDTH,
|
width = A_WIDTH,
|
||||||
height = A_HEIGHT,
|
height = A_HEIGHT,
|
||||||
|
@ -79,7 +79,7 @@ class FetchMediaUseCaseTest {
|
||||||
result shouldBeEqualTo listOf(
|
result shouldBeEqualTo listOf(
|
||||||
Media(
|
Media(
|
||||||
id = A_ROW_ID,
|
id = A_ROW_ID,
|
||||||
uri = appendedUri.instance,
|
uri = ROW_URI.instance,
|
||||||
mimeType = A_MIME_TYPE,
|
mimeType = A_MIME_TYPE,
|
||||||
width = A_HEIGHT,
|
width = A_HEIGHT,
|
||||||
height = A_WIDTH,
|
height = A_WIDTH,
|
||||||
|
|
Loading…
Reference in New Issue