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 givenFile(uri: Uri) = every { instance.openInputStream(uri) }.delegateReturn()
|
||||||
|
|
||||||
fun givenUriResult(uri: Uri) = every { instance.query(uri, null, null, null, null) }.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.getColumnIndex(columnName) } returns columnId
|
||||||
every { instance.getString(columnId) } returns content
|
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
|
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
|
import android.provider.MediaStore
|
||||||
|
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 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._ID,
|
||||||
MediaStore.Images.Media.MIME_TYPE,
|
MediaStore.Images.Media.MIME_TYPE,
|
||||||
MediaStore.Images.Media.DATE_MODIFIED,
|
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 ?"
|
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> {
|
suspend fun getMediaInBucket(bucketId: String): List<Media> {
|
||||||
|
|
||||||
return dispatchers.withIoContext {
|
return dispatchers.withIoContext {
|
||||||
val media = mutableListOf<Media>()
|
val query = ContentResolverQuery(
|
||||||
val selectionArgs = arrayOf(bucketId, "%image/svg%")
|
uri = uriAvoidance.externalContentUri,
|
||||||
val sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC"
|
projection = projection,
|
||||||
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
selection = selection,
|
||||||
contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor ->
|
selectionArgs = listOf(bucketId, "%image/svg%"),
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC",
|
||||||
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
|
)
|
||||||
val uri = ContentUris.withAppendedId(contentUri, rowId)
|
|
||||||
|
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 mimetype = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE))
|
||||||
val date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED))
|
val date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED))
|
||||||
val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.ORIENTATION))
|
val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.ORIENTATION))
|
||||||
val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)))
|
val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)))
|
||||||
val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)))
|
val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)))
|
||||||
val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))
|
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
|
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) =
|
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(
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
package app.dapk.st.messenger.gallery
|
package app.dapk.st.messenger.gallery
|
||||||
|
|
||||||
import android.content.ContentResolver
|
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.ImageGalleryState
|
||||||
import app.dapk.st.messenger.gallery.state.imageGalleryReducer
|
import app.dapk.st.messenger.gallery.state.imageGalleryReducer
|
||||||
|
|
||||||
|
@ -14,7 +20,14 @@ class ImageGalleryModule(
|
||||||
imageGalleryReducer(
|
imageGalleryReducer(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
FetchMediaFoldersUseCase(contentResolver, dispatchers),
|
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(),
|
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