From 42761c0899fd2011f86b0f86ba0e20b10055c063 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 3 Nov 2022 11:28:27 +0000 Subject: [PATCH] adding test for fetching media folders --- .../dapk/st/core/ContentResolverExtensions.kt | 13 ++- .../gallery/FetchMediaFoldersUseCase.kt | 38 ++++---- .../st/messenger/gallery/FetchMediaUseCase.kt | 7 +- .../messenger/gallery/ImageGalleryModule.kt | 16 ++-- .../st/messenger/gallery/MediaUriAvoidance.kt | 8 ++ .../gallery/FetchMediaFoldersUseCaseTest.kt | 91 +++++++++++++++++++ .../gallery/FetchMediaUseCaseTest.kt | 10 +- 7 files changed, 141 insertions(+), 42 deletions(-) create mode 100644 features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/MediaUriAvoidance.kt create mode 100644 features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCaseTest.kt diff --git a/domains/android/core/src/main/kotlin/app/dapk/st/core/ContentResolverExtensions.kt b/domains/android/core/src/main/kotlin/app/dapk/st/core/ContentResolverExtensions.kt index 0d1161b..ea6d723 100644 --- a/domains/android/core/src/main/kotlin/app/dapk/st/core/ContentResolverExtensions.kt +++ b/domains/android/core/src/main/kotlin/app/dapk/st/core/ContentResolverExtensions.kt @@ -13,11 +13,18 @@ data class ContentResolverQuery( ) inline fun ContentResolver.reduce(query: ContentResolverQuery, operation: (Cursor) -> T): List { - val result = mutableListOf() + return this.reduce(query, mutableListOf()) { acc, cursor -> + acc.add(operation(cursor)) + acc + } +} + +inline fun 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 } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt index 1f17102..1c2b241 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt @@ -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 { 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() - 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()) { 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() } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCase.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCase.kt index 0b0414d..dc81d3c 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCase.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCase.kt @@ -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( diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt index 7016eb7..11ef676 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt @@ -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(), ) } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/MediaUriAvoidance.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/MediaUriAvoidance.kt new file mode 100644 index 0000000..4949083 --- /dev/null +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/MediaUriAvoidance.kt @@ -0,0 +1,8 @@ +package app.dapk.st.messenger.gallery + +import android.net.Uri + +class MediaUriAvoidance( + val uriAppender: (Uri, Long) -> Uri, + val externalContentUri: Uri, +) \ No newline at end of file diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCaseTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCaseTest.kt new file mode 100644 index 0000000..1331bcb --- /dev/null +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCaseTest.kt @@ -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() + + override fun invoke(uri: Uri, rowId: Long): Uri { + val fakeUri = FakeUri() + uris[rowId] = fakeUri + return fakeUri.instance + } + + fun get(rowId: Long) = uris[rowId]!!.instance +} \ No newline at end of file diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCaseTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCaseTest.kt index 3f70485..799a5b4 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCaseTest.kt +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCaseTest.kt @@ -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,