1 year ago

#318151

test-img

Frank Egan

ContentResolver from ProviderTestRule doesn't notify ContentObserver

I'm trying to test my Flow based ContentResolver extension function:

abstract class Query {
    abstract suspend fun runQuery(): Cursor?
}

private val mainThread = Handler(Looper.getMainLooper())

fun ContentResolver.observeQuery(
    uri: Uri,
    projection: Array<String>? = null,
    selection: String? = null,
    selectionArgs: Array<String>? = null,
    sortOrder: String? = null,
    notifyForDescendants: Boolean = true
): Flow<Query> {
    val query: Query = object : Query() {
        override suspend fun runQuery(): Cursor? {
            return query(uri, projection, selection, selectionArgs, sortOrder)
        }
    }

    return callbackFlow {
        val observer: ContentObserver = object : ContentObserver(mainThread) {
            override fun onChange(selfChange: Boolean) {
                trySend(query)
            }
        }

        registerContentObserver(uri, notifyForDescendants, observer)

        awaitClose {
            unregisterContentObserver(observer)
        }
    }.onStart {
        emit(query)
    }
}

Here's my androidTest code written using Cash App's Turbine Flow testing library.

class KiteContentResolverTest {

    private val dispatcher = TestCoroutineDispatcher()

    @get:Rule
    val providerRule: ProviderTestRule = ProviderTestRule
        .Builder(TestContentProvider::class.java, AUTHORITY.authority!!)
        .build()

    private val contentResolver: ContentResolver
        get() = providerRule.resolver

    @Test
    fun testCreateQueryObservesInsert() = runBlockingTest {
        contentResolver.observeQuery(TABLE).flowOn(dispatcher).test {
            awaitItem().runQuery()!!.isExhausted()
            contentResolver.insert(TABLE, values("key1", "val1"))
            //Code suspends here and never receives another item.
            awaitItem().runQuery()!!
                .hasRow("key1", "val1")
                .isExhausted()
        }
    }

    @Test
    fun testCreateQueryObservesUpdate() = runBlockingTest {
        contentResolver.insert(TABLE, values("key1", "val1"))
        contentResolver.observeQuery(TABLE).flowOn(dispatcher).test {
            awaitItem().runQuery()!!.hasRow("key1", "val1").isExhausted()
            contentResolver.update(TABLE, values("key1", "val2"), null, null)
            //Code suspends here and never receives another item.
            awaitItem().runQuery()!!.hasRow("key1", "val2").isExhausted()
        }
    }

    companion object {

        private val AUTHORITY: Uri = Uri.parse("content://test_authority")
        private val TABLE: Uri = AUTHORITY.buildUpon().appendPath("test_table").build()
        private const val KEY = "test_key"
        private const val VALUE = "test_value"
    }
}

And my mock ContentProvider

class TestContentProvider: ContentProvider() {
    private val storage: MutableMap<String, String> = LinkedHashMap()
    
    private val contentResolver: ContentResolver
        get() = context!!.contentResolver
    
    ...
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        values ?: return null
        storage[values.getAsString(KEY)] = values.getAsString(VALUE)
        contentResolver.notifyChange(uri, null)
        return Uri.parse(AUTHORITY.toString() + "/" + values.getAsString(KEY))
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        values ?: return storage.size
        for (key in storage.keys) {
            storage[key] = values.getAsString(VALUE)
        }
        contentResolver.notifyChange(uri, null)
        return storage.size
    }

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor {
        val result = MatrixCursor(arrayOf(KEY, VALUE))
        for ((key, value) in storage) {
            result.addRow(arrayOf<Any>(key, value))
        }
        return result
    }
}

However, It seems like the ContentObserver is never actually called when my data is inserted or updated. And my tests always fail with this stack trace:

kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms

Oddly, enough when I run this code on a real device using the actual MediaStore ContentResolver the code executes and the flow emits new values as expected.

It would seem to me that the ContentResolver provided by the ProviderTestRule simply doesn't work with content observers. Has anyone else figured out a way to observer mock ContentResolvers in this way?

android

kotlin-coroutines

android-contentresolver

kotlin-flow

turbine

0 Answers

Your Answer

Accepted video resources