Gil
Gil

Reputation: 31

How to handle Room functions in Android's ContentProvider?

I'm trying to provide data from SQLite with Room library in ContentProvider and my app uses Hilt for dependency injection.

@Entity(tableName = "files")
data class FileEntity(
    @PrimaryKey(autoGenerate = false)
    val url: String,
    val fileName: String
)
@Dao
interface FilesDao {
    @Query(value = "SELECT * FROM files ORDER BY fileName")
    suspend fun getFiles(): List<FileEntity>
}
@Database(entities = [FileEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract val filesDao: FilesDao
}
@Module
@InstallIn(SingletonComponent::class)
object RoomModule {
    @Provides
    @Singleton
    fun provideAppDatabase(
        @ApplicationContext context: Context
    ): AppDatabase = Room.databaseBuilder(
        context, AppDatabase::class.java, "app_database"
    ).build()

    @Provides
    @Singleton
    fun provideFilesDao(
        appDatabase: AppDatabase
    ): FilesDao = appDatabase.filesDao
}

With the code above I can receive an instance of a FilesDao in a DataSource class and make the calls normally through suspended functions. For example:

data class FileModel(
    val url: String,
    val fileName: String
)
interface FilesDataSource {
    suspend fun getFiles(): List<FileModel>
}

class FilesDataSourceImpl @Inject constructor(
    private val filesDao: FilesDao
) : FilesDataSource {
    override suspend fun getFiles(): List<FileModel>{
        val filesEntity = filesDao.getFiles()
        return filesEntity.toFilesModel()
    }

    private fun List<FileEntity>.toFilesModel() = map { fileEntity ->
        FileModel(
            url = fileEntity.url,
            fileName = fileEntity.fileName
        )
    }
}

To pass the instance via dependency injection using Hilt to a ContentProvider I know that it is necessary to create a custom EntryPoint. For example:

@EntryPoint
@InstallIn(SingletonComponent::class)
interface ContentProviderEntryPoint {
    // var appDatabase: AppDatabase
    var filesDao: FilesDao
}

In ContentProvider I have the following code so far:

class MyProvider : ContentProvider() {
    private val appContext: Context by lazy {
        context?.applicationContext ?: throw IllegalStateException()
    }

    private val files: List<FileModel> by lazy {
        val hiltEntryPoint = EntryPointAccessors.fromApplication(
            context = appContext,
            entryPoint = ContentProviderEntryPoint::class.java
        )

        hiltEntryPoint.filesDao.getFiles() // this is a suspend function and I have no idea how to deal...
    }

    override fun onCreate(): Boolean {
        return true
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor {
        TODO()
    }

    override fun getType(uri: Uri): String {
        TODO()
    }

    override fun openAssetFile(
        uri: Uri,
        mode: String
    ): AssetFileDescriptor? {
        TODO()
    }

    override fun insert(
        uri: Uri,
        values: ContentValues?
    ): Uri = throw UnsupportedOperationException()

    override fun delete(
        uri: Uri,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int = throw UnsupportedOperationException()

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int = throw UnsupportedOperationException()
}

Now my doubt, how to deal with the suspended function inside the ContentProvider?

Additional:
I've seen some examples where a new function (not suspended) was created in the DAO class to return a Cursor. For example:

@Dao
interface FilesDao {
    @Query(value = "SELECT * FROM files")
    fun getFilesCursor(): Cursor

    // other suspend functions for datasource layer
}

That way I can call the new function in the ContentProvider, but Room throws an exception:

class MyProvider : ContentProvider() {
    private val appContext: Context by lazy {
        context?.applicationContext ?: throw IllegalStateException()
    }

    private val filesCursor: Cursor by lazy {
        val hiltEntryPoint = EntryPointAccessors.fromApplication(
            context = appContext,
            entryPoint = ContentProviderEntryPoint::class.java
        )

        hiltEntryPoint.filesDao.getFilesCursor()
    }

    override fun onCreate(): Boolean {
        println(filesCursor) // just to see the output, but this throw an IllegalStateException
        return true
    }

    // ...
}

java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

My goal with ContentProvider is to provide Stickers (files) for WhatsApp. I already have a working version of the app that reads the content directly from a JSON file following the same logic with Hilt, the issue is that now that I'm migrating to Room that uses suspended functions I'm not able to deal with this situation.

Upvotes: 3

Views: 646

Answers (1)

Kirill Efremov
Kirill Efremov

Reputation: 1

You can use runBlocking inside your query method:

override fun query(
    uri: Uri,
    projection: Array<out String>?,
    selection: String?,
    selectionArgs: Array<out String>?,
    sortOrder: String?
): Cursor {
    return runBlocking {
        // your code here
    }
}

But you should call query from CoroutineScope(Dispatchers.IO) because runBlocking blocks the current thread interruptibly until its completion

Upvotes: 0

Related Questions