diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9604b42..c61ec24 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,7 @@ sqldelight { databases { create("Database") { packageName.set("nl.ndat.tvlauncher.data.sqldelight") + generateAsync.set(true) } } } diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/data/DatabaseContainer.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/data/DatabaseContainer.kt index 6cbd9cf..ded311f 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/data/DatabaseContainer.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/data/DatabaseContainer.kt @@ -3,8 +3,9 @@ package nl.ndat.tvlauncher.data import android.content.Context import app.cash.sqldelight.ColumnAdapter import app.cash.sqldelight.EnumColumnAdapter -import app.cash.sqldelight.TransactionWithReturn -import app.cash.sqldelight.TransactionWithoutReturn +import app.cash.sqldelight.SuspendingTransactionWithReturn +import app.cash.sqldelight.SuspendingTransactionWithoutReturn +import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.driver.android.AndroidSqliteDriver import nl.ndat.tvlauncher.data.sqldelight.Channel import nl.ndat.tvlauncher.data.sqldelight.ChannelProgram @@ -18,7 +19,7 @@ class DatabaseContainer( const val DB_FILE = "data.db" } - private val driver = AndroidSqliteDriver(Database.Schema, context, DB_FILE) + private val driver = AndroidSqliteDriver(Database.Schema.synchronous(), context, DB_FILE) private val database = Database( driver = driver, ChannelAdapter = Channel.Adapter( @@ -43,8 +44,8 @@ class DatabaseContainer( val channels = database.channelQueries val channelPrograms = database.channelProgramQueries - fun transaction(body: TransactionWithoutReturn.() -> Unit) = database.transaction { body() } - fun transactionForResult(body: TransactionWithReturn.() -> T) = database.transactionWithResult { body() } + suspend fun transaction(body: suspend SuspendingTransactionWithoutReturn.() -> Unit) = database.transaction { body() } + suspend fun transactionForResult(body: suspend SuspendingTransactionWithReturn.() -> T) = database.transactionWithResult { body() } } class IntColumnAdapter : ColumnAdapter { diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/data/queryExtensions.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/data/queryExtensions.kt index a4a8f04..e3387ee 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/data/queryExtensions.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/data/queryExtensions.kt @@ -2,6 +2,7 @@ package nl.ndat.tvlauncher.data import app.cash.sqldelight.Query import app.cash.sqldelight.coroutines.asFlow -import kotlinx.coroutines.flow.map +import app.cash.sqldelight.coroutines.mapToList +import kotlinx.coroutines.Dispatchers -fun Query.executeAsListFlow() = asFlow().map { it.executeAsList() } +fun Query.executeAsListFlow() = asFlow().mapToList(Dispatchers.IO) diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/AppRepository.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/AppRepository.kt index 8874a06..5b1bec5 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/AppRepository.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/AppRepository.kt @@ -1,6 +1,9 @@ package nl.ndat.tvlauncher.data.repository import android.content.Context +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import nl.ndat.tvlauncher.data.DatabaseContainer import nl.ndat.tvlauncher.data.executeAsListFlow import nl.ndat.tvlauncher.data.resolver.AppResolver @@ -11,16 +14,18 @@ class AppRepository( private val appResolver: AppResolver, private val database: DatabaseContainer, ) { - private suspend fun commitApps(apps: Collection) = database.transaction { - // Remove missing apps from database - val currentIds = apps.map { it.id } - database.apps.removeNotIn(currentIds) - - // Upsert apps - apps.map { app -> commitApp(app) } + private suspend fun commitApps(apps: Collection) = withContext(Dispatchers.IO) { + database.transaction { + // Remove missing apps from database + val currentIds = apps.map { it.id } + database.apps.removeNotIn(currentIds) + + // Upsert apps + apps.map { app -> commitApp(app) } + } } - private fun commitApp(app: App) { + private suspend fun commitApp(app: App) = withContext(Dispatchers.IO) { database.apps.upsert( displayName = app.displayName, packageName = app.packageName, @@ -30,12 +35,12 @@ class AppRepository( ) } - suspend fun refreshAllApplications() { + suspend fun refreshAllApplications() = withContext(Dispatchers.IO) { val apps = appResolver.getApplications(context) commitApps(apps) } - suspend fun refreshApplication(packageName: String) { + suspend fun refreshApplication(packageName: String) = withContext(Dispatchers.IO) { val app = appResolver.getApplication(context, packageName) if (app == null) database.apps.removeByPackageName(packageName) @@ -44,9 +49,9 @@ class AppRepository( fun getApps() = database.apps.getAll().executeAsListFlow() fun getFavoriteApps() = database.apps.getAllFavorites(::App).executeAsListFlow() - suspend fun getByPackageName(packageName: String) = database.apps.getByPackageName(packageName).executeAsOneOrNull() + suspend fun getByPackageName(packageName: String) = withContext(Dispatchers.IO) { database.apps.getByPackageName(packageName).awaitAsOneOrNull() } - fun favorite(id: String) = database.apps.updateFavoriteAdd(id) - fun unfavorite(id: String) = database.apps.updateFavoriteRemove(id) - fun updateFavoriteOrder(id: String, order: Int) = database.apps.updateFavoriteOrder(id, order.toLong()) + suspend fun favorite(id: String) = withContext(Dispatchers.IO) { database.apps.updateFavoriteAdd(id) } + suspend fun unfavorite(id: String) = withContext(Dispatchers.IO) { database.apps.updateFavoriteRemove(id) } + suspend fun updateFavoriteOrder(id: String, order: Int) = withContext(Dispatchers.IO) { database.apps.updateFavoriteOrder(id, order.toLong()) } } diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/ChannelRepository.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/ChannelRepository.kt index 410608b..2aebc82 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/ChannelRepository.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/ChannelRepository.kt @@ -1,6 +1,8 @@ package nl.ndat.tvlauncher.data.repository import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import nl.ndat.tvlauncher.data.DatabaseContainer import nl.ndat.tvlauncher.data.executeAsListFlow import nl.ndat.tvlauncher.data.model.ChannelType @@ -13,7 +15,7 @@ class ChannelRepository( private val channelResolver: ChannelResolver, private val database: DatabaseContainer, ) { - private suspend fun commitChannels(type: ChannelType, channels: Collection) = + private suspend fun commitChannels(type: ChannelType, channels: Collection) = withContext(Dispatchers.IO) { database.transaction { // Remove missing channels from database val currentIds = channels.map { it.id } @@ -22,8 +24,10 @@ class ChannelRepository( // Upsert channels channels.map { channel -> commitChannel(channel) } } + } + - private fun commitChannel(channel: Channel) { + private suspend fun commitChannel(channel: Channel) = withContext(Dispatchers.IO) { database.channels.upsert( id = channel.id, type = channel.type, @@ -38,16 +42,18 @@ class ChannelRepository( private suspend fun commitChannelPrograms( channelId: String, programs: Collection, - ) = database.transaction { - // Remove missing channels from database - val currentIds = programs.map { it.id } - database.channelPrograms.removeNotIn(channelId, currentIds) + ) = withContext(Dispatchers.IO) { + database.transaction { + // Remove missing channels from database + val currentIds = programs.map { it.id } + database.channelPrograms.removeNotIn(channelId, currentIds) - // Upsert channels - programs.map { program -> commitChannelProgram(program) } + // Upsert channels + programs.map { program -> commitChannelProgram(program) } + } } - private fun commitChannelProgram(program: ChannelProgram) { + private suspend fun commitChannelProgram(program: ChannelProgram) = withContext(Dispatchers.IO) { database.channelPrograms.upsert( id = program.id, channelId = program.channelId, diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/InputRepository.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/InputRepository.kt index 3613b80..bf9dc8d 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/InputRepository.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/data/repository/InputRepository.kt @@ -1,6 +1,8 @@ package nl.ndat.tvlauncher.data.repository import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import nl.ndat.tvlauncher.data.DatabaseContainer import nl.ndat.tvlauncher.data.executeAsListFlow import nl.ndat.tvlauncher.data.resolver.InputResolver @@ -11,16 +13,18 @@ class InputRepository( private val inputResolver: InputResolver, private val database: DatabaseContainer ) { - private suspend fun commitInputs(inputs: Collection) = database.transaction { - // Remove missing inputs from database - val currentIds = inputs.map { it.id } - database.inputs.removeNotIn(currentIds) + private suspend fun commitInputs(inputs: Collection) = withContext(Dispatchers.IO) { + database.transaction { + // Remove missing inputs from database + val currentIds = inputs.map { it.id } + database.inputs.removeNotIn(currentIds) - // Upsert inputs - inputs.map { input -> commitInput(input) } + // Upsert inputs + inputs.map { input -> commitInput(input) } + } } - private fun commitInput(input: Input) { + private suspend fun commitInput(input: Input) = withContext(Dispatchers.IO) { database.inputs.upsert( id = input.id, inputId = input.id, diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/data/resolver/ChannelResolver.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/data/resolver/ChannelResolver.kt index fa90cfb..59989b5 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/data/resolver/ChannelResolver.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/data/resolver/ChannelResolver.kt @@ -12,6 +12,8 @@ import androidx.tvprovider.media.tv.PreviewChannel import androidx.tvprovider.media.tv.PreviewProgram import androidx.tvprovider.media.tv.TvContractCompat import androidx.tvprovider.media.tv.WatchNextProgram +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import nl.ndat.tvlauncher.data.model.ChannelProgramAspectRatio import nl.ndat.tvlauncher.data.model.ChannelProgramInteractionType import nl.ndat.tvlauncher.data.model.ChannelProgramType @@ -50,16 +52,16 @@ class ChannelResolver { null } - fun getPreviewChannels(context: Context): List { + suspend fun getPreviewChannels(context: Context): List = withContext(Dispatchers.IO) { val cursor = context.contentResolver.tryQuery( TvContractCompat.Channels.CONTENT_URI, PreviewChannel.Columns.PROJECTION, - ) ?: return emptyList() + ) ?: return@withContext emptyList() - return buildList { + buildList { if (!cursor.moveToFirst()) { Timber.w("Unable to move cursor") - return emptyList() + return@buildList } do { @@ -86,16 +88,16 @@ class ChannelResolver { } } - fun getChannelPrograms(context: Context, channelId: Long): List { + suspend fun getChannelPrograms(context: Context, channelId: Long): List = withContext(Dispatchers.IO) { val cursor = context.contentResolver.tryQuery( TvContractCompat.buildPreviewProgramsUriForChannel(channelId), PreviewProgram.PROJECTION, - ) ?: return emptyList() + ) ?: return@withContext emptyList() - return buildList { + buildList { if (!cursor.moveToFirst()) { Timber.w("Unable to move cursor") - return emptyList() + return@buildList } do { @@ -112,16 +114,16 @@ class ChannelResolver { } } - fun getWatchNextPrograms(context: Context): List { + suspend fun getWatchNextPrograms(context: Context): List = withContext(Dispatchers.IO) { val cursor = context.contentResolver.tryQuery( TvContractCompat.WatchNextPrograms.CONTENT_URI, WatchNextProgram.PROJECTION, - ) ?: return emptyList() + ) ?: return@withContext emptyList() - return buildList { + buildList { if (!cursor.moveToFirst()) { Timber.w("Unable to move cursor") - return emptyList() + return@buildList } do { diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/apps/AppsTabViewModel.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/apps/AppsTabViewModel.kt index 5a1d027..9c9a4c4 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/apps/AppsTabViewModel.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/apps/AppsTabViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import nl.ndat.tvlauncher.BuildConfig import nl.ndat.tvlauncher.data.repository.AppRepository import nl.ndat.tvlauncher.data.sqldelight.App @@ -17,7 +18,7 @@ class AppsTabViewModel( .map { apps -> apps.filterNot { app -> app.packageName == BuildConfig.APPLICATION_ID } } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) - fun favoriteApp(app: App, favorite: Boolean) { + fun favoriteApp(app: App, favorite: Boolean) = viewModelScope.launch { if (favorite) appRepository.favorite(app.id) else appRepository.unfavorite(app.id) } diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/home/HomeTabViewModel.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/home/HomeTabViewModel.kt index 197e8de..ae2c8f2 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/home/HomeTabViewModel.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/home/HomeTabViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import nl.ndat.tvlauncher.data.repository.AppRepository import nl.ndat.tvlauncher.data.repository.ChannelRepository import nl.ndat.tvlauncher.data.sqldelight.App @@ -21,15 +22,15 @@ class HomeTabViewModel( fun channelPrograms(channel: Channel) = channelRepository.getProgramsByChannel(channel) - fun favoriteApp(app: App, favorite: Boolean) { + fun favoriteApp(app: App, favorite: Boolean) = viewModelScope.launch { // Return is state is already satisfied - if ((app.favoriteOrder != null) == favorite) return + if ((app.favoriteOrder != null) == favorite) return@launch if (favorite) appRepository.favorite(app.id) else appRepository.unfavorite(app.id) } - fun setFavoriteOrder(app: App, order: Int) { + fun setFavoriteOrder(app: App, order: Int) = viewModelScope.launch { // Make sure app is favorite first if (app.favoriteOrder == null) appRepository.favorite(app.id) appRepository.updateFavoriteOrder(app.id, order)