adding media fetching tests
This commit is contained in:
parent
40534bc581
commit
dcf4b4c80a
|
@ -0,0 +1,23 @@
|
|||
package app.dapk.st.core
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
|
||||
data class ContentResolverQuery(
|
||||
val uri: Uri,
|
||||
val projection: List<String>,
|
||||
val selection: String,
|
||||
val selectionArgs: List<String>,
|
||||
val sortBy: String,
|
||||
)
|
||||
|
||||
inline fun <T> ContentResolver.reduce(query: ContentResolverQuery, operation: (Cursor) -> T): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
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))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -13,4 +13,20 @@ class FakeContentResolver {
|
|||
fun givenFile(uri: Uri) = every { instance.openInputStream(uri) }.delegateReturn()
|
||||
|
||||
fun givenUriResult(uri: Uri) = every { instance.query(uri, null, null, null, null) }.delegateReturn()
|
||||
|
||||
fun givenQueryResult(
|
||||
uri: Uri,
|
||||
projection: Array<String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String>?,
|
||||
sortOrder: String?,
|
||||
) = every {
|
||||
instance.query(
|
||||
uri,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
sortOrder
|
||||
)
|
||||
}.delegateReturn()
|
||||
}
|
||||
|
|
|
@ -24,4 +24,56 @@ class FakeCursor {
|
|||
every { instance.getColumnIndex(columnName) } returns columnId
|
||||
every { instance.getString(columnId) } returns content
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface CreateCursorScope {
|
||||
fun addRow(vararg item: Pair<String, Any?>)
|
||||
}
|
||||
|
||||
fun createCursor(creator: CreateCursorScope.() -> Unit): Cursor {
|
||||
val content = mutableListOf<Map<String, Any?>>()
|
||||
val scope = object : CreateCursorScope {
|
||||
override fun addRow(vararg item: Pair<String, Any?>) {
|
||||
content.add(item.toMap())
|
||||
}
|
||||
}
|
||||
creator(scope)
|
||||
return StubCursor(content)
|
||||
}
|
||||
|
||||
private class StubCursor(private val content: List<Map<String, Any?>>) : Cursor by mockk() {
|
||||
|
||||
private val columnNames = content.map { it.keys }.flatten().distinct()
|
||||
private var currentRowIndex = -1
|
||||
|
||||
override fun getColumnIndexOrThrow(columnName: String): Int {
|
||||
return getColumnIndex(columnName).takeIf { it != -1 } ?: throw IllegalArgumentException(columnName)
|
||||
}
|
||||
|
||||
override fun getColumnIndex(columnName: String) = columnNames.indexOf(columnName)
|
||||
|
||||
override fun moveToNext() = (currentRowIndex + 1 < content.size).also {
|
||||
currentRowIndex += 1
|
||||
}
|
||||
|
||||
override fun moveToFirst() = content.isNotEmpty()
|
||||
|
||||
override fun getCount() = content.size
|
||||
|
||||
override fun getString(index: Int): String? = content[currentRowIndex][columnNames[index]] as? String
|
||||
|
||||
override fun getInt(index: Int): Int {
|
||||
return content[currentRowIndex][columnNames[index]] as? Int ?: throw IllegalArgumentException("Int can't be null")
|
||||
}
|
||||
|
||||
override fun getLong(index: Int): Long {
|
||||
return content[currentRowIndex][columnNames[index]] as? Long ?: throw IllegalArgumentException("Long can't be null")
|
||||
}
|
||||
|
||||
override fun getColumnCount() = columnNames.size
|
||||
|
||||
override fun close() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
|
@ -1,15 +1,20 @@
|
|||
package app.dapk.st.messenger.gallery
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
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 FetchMediaUseCase(private val contentResolver: ContentResolver, private val dispatchers: CoroutineDispatchers) {
|
||||
class FetchMediaUseCase(
|
||||
private val contentResolver: ContentResolver,
|
||||
private val uriAvoidance: UriAvoidance,
|
||||
private val dispatchers: CoroutineDispatchers
|
||||
) {
|
||||
|
||||
private val projection = arrayOf(
|
||||
private val projection = listOf(
|
||||
MediaStore.Images.Media._ID,
|
||||
MediaStore.Images.Media.MIME_TYPE,
|
||||
MediaStore.Images.Media.DATE_MODIFIED,
|
||||
|
@ -22,27 +27,27 @@ class FetchMediaUseCase(private val contentResolver: ContentResolver, private va
|
|||
private val selection = MediaStore.Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + MediaStore.Images.Media.MIME_TYPE + " NOT LIKE ?"
|
||||
|
||||
suspend fun getMediaInBucket(bucketId: String): List<Media> {
|
||||
|
||||
return dispatchers.withIoContext {
|
||||
val media = mutableListOf<Media>()
|
||||
val selectionArgs = arrayOf(bucketId, "%image/svg%")
|
||||
val sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC"
|
||||
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor ->
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
|
||||
val uri = ContentUris.withAppendedId(contentUri, rowId)
|
||||
val query = ContentResolverQuery(
|
||||
uri = uriAvoidance.externalContentUri,
|
||||
projection = projection,
|
||||
selection = selection,
|
||||
selectionArgs = listOf(bucketId, "%image/svg%"),
|
||||
sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC",
|
||||
)
|
||||
|
||||
contentResolver.reduce(query) { cursor ->
|
||||
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
|
||||
val uri = uriAvoidance.uriAppender(query.uri, rowId)
|
||||
val mimetype = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE))
|
||||
val date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED))
|
||||
val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.ORIENTATION))
|
||||
val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)))
|
||||
val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)))
|
||||
val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))
|
||||
media.add(Media(rowId, uri, mimetype, width, height, size, date))
|
||||
Media(rowId, uri, mimetype, width, height, size, date)
|
||||
}
|
||||
}
|
||||
media
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWidthColumn(orientation: Int) = if (orientation == 0 || orientation == 180) MediaStore.Images.Media.WIDTH else MediaStore.Images.Media.HEIGHT
|
||||
|
@ -50,6 +55,10 @@ class FetchMediaUseCase(private val contentResolver: ContentResolver, private va
|
|||
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(
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
package app.dapk.st.messenger.gallery
|
||||
|
||||
import android.content.ContentResolver
|
||||
import app.dapk.st.core.*
|
||||
import android.content.ContentUris
|
||||
import android.provider.MediaStore
|
||||
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
|
||||
|
||||
|
@ -14,7 +20,14 @@ class ImageGalleryModule(
|
|||
imageGalleryReducer(
|
||||
roomName = roomName,
|
||||
FetchMediaFoldersUseCase(contentResolver, dispatchers),
|
||||
FetchMediaUseCase(contentResolver, dispatchers),
|
||||
FetchMediaUseCase(
|
||||
contentResolver,
|
||||
UriAvoidance(
|
||||
uriAppender = { uri, rowId -> ContentUris.withAppendedId(uri, rowId) },
|
||||
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
),
|
||||
dispatchers,
|
||||
),
|
||||
JobBag(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package app.dapk.st.messenger.gallery
|
||||
|
||||
import android.provider.MediaStore
|
||||
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_ROW_ID = 20L
|
||||
private const val A_MIME_TYPE = "image/png"
|
||||
private const val A_DATE_MODIFIED = 5000L
|
||||
private const val A_SIZE = 1000L
|
||||
private const val A_NORMAL_ORIENTATION = 0
|
||||
private const val AN_INVERTED_ORIENTATION = 90
|
||||
private const val A_WIDTH = 250
|
||||
private const val A_HEIGHT = 750
|
||||
|
||||
class FetchMediaUseCaseTest {
|
||||
|
||||
private val fakeContentResolver = FakeContentResolver()
|
||||
private val appendedUri = FakeUri()
|
||||
private val uriAvoidance = FetchMediaUseCase.UriAvoidance(
|
||||
uriAppender = { _, _ -> appendedUri.instance },
|
||||
externalContentUri = A_EXTERNAL_CONTENT_URI.instance,
|
||||
)
|
||||
|
||||
private val useCase = FetchMediaUseCase(fakeContentResolver.instance, uriAvoidance, aCoroutineDispatchers())
|
||||
|
||||
@Test
|
||||
fun `given cursor content, when get media for bucket, then reads media`() = runTest {
|
||||
fakeContentResolver.givenMediaQuery().returns(createCursor {
|
||||
addRow(
|
||||
MediaStore.Images.Media._ID to A_ROW_ID,
|
||||
MediaStore.Images.Media.MIME_TYPE to A_MIME_TYPE,
|
||||
MediaStore.Images.Media.DATE_MODIFIED to A_DATE_MODIFIED,
|
||||
MediaStore.Images.Media.ORIENTATION to A_NORMAL_ORIENTATION,
|
||||
MediaStore.Images.Media.WIDTH to A_WIDTH,
|
||||
MediaStore.Images.Media.HEIGHT to A_HEIGHT,
|
||||
MediaStore.Images.Media.SIZE to A_SIZE,
|
||||
)
|
||||
})
|
||||
|
||||
val result = useCase.getMediaInBucket(A_BUCKET_ID)
|
||||
|
||||
result shouldBeEqualTo listOf(
|
||||
Media(
|
||||
id = A_ROW_ID,
|
||||
uri = appendedUri.instance,
|
||||
mimeType = A_MIME_TYPE,
|
||||
width = A_WIDTH,
|
||||
height = A_HEIGHT,
|
||||
size = A_SIZE,
|
||||
dateModifiedEpochMillis = A_DATE_MODIFIED
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given cursor content with 90 degree orientation, when get media for bucket, then reads media with inverted width and height`() = runTest {
|
||||
fakeContentResolver.givenMediaQuery().returns(createCursor {
|
||||
addRow(
|
||||
MediaStore.Images.Media._ID to A_ROW_ID,
|
||||
MediaStore.Images.Media.MIME_TYPE to A_MIME_TYPE,
|
||||
MediaStore.Images.Media.DATE_MODIFIED to A_DATE_MODIFIED,
|
||||
MediaStore.Images.Media.ORIENTATION to AN_INVERTED_ORIENTATION,
|
||||
MediaStore.Images.Media.WIDTH to A_WIDTH,
|
||||
MediaStore.Images.Media.HEIGHT to A_HEIGHT,
|
||||
MediaStore.Images.Media.SIZE to A_SIZE,
|
||||
)
|
||||
})
|
||||
|
||||
val result = useCase.getMediaInBucket(A_BUCKET_ID)
|
||||
|
||||
result shouldBeEqualTo listOf(
|
||||
Media(
|
||||
id = A_ROW_ID,
|
||||
uri = appendedUri.instance,
|
||||
mimeType = A_MIME_TYPE,
|
||||
width = A_HEIGHT,
|
||||
height = A_WIDTH,
|
||||
size = A_SIZE,
|
||||
dateModifiedEpochMillis = A_DATE_MODIFIED
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun FakeContentResolver.givenMediaQuery() = this.givenQueryResult(
|
||||
A_EXTERNAL_CONTENT_URI.instance,
|
||||
arrayOf(
|
||||
MediaStore.Images.Media._ID,
|
||||
MediaStore.Images.Media.MIME_TYPE,
|
||||
MediaStore.Images.Media.DATE_MODIFIED,
|
||||
MediaStore.Images.Media.ORIENTATION,
|
||||
MediaStore.Images.Media.WIDTH,
|
||||
MediaStore.Images.Media.HEIGHT,
|
||||
MediaStore.Images.Media.SIZE
|
||||
),
|
||||
MediaStore.Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + MediaStore.Images.Media.MIME_TYPE + " NOT LIKE ?",
|
||||
arrayOf(A_BUCKET_ID, "%image/svg%"),
|
||||
MediaStore.Images.Media.DATE_MODIFIED + " DESC",
|
||||
)
|
Loading…
Reference in New Issue