diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 41460207..c8bc4355 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -149,6 +149,7 @@ dependencies { implementation(libs.androidx.core) implementation(libs.androidx.fragment) implementation(libs.androidx.hilt.work) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.base) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.paging) diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/SettingsManagerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/SettingsManagerTest.kt index e598ef2e..976acde2 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/SettingsManagerTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/SettingsManagerTest.kt @@ -5,18 +5,15 @@ package at.bitfire.davdroid.settings import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.map -import at.bitfire.davdroid.TestUtils.getOrAwaitValue import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -62,51 +59,35 @@ class SettingsManagerTest { @Test - fun test_getBooleanLive_initialValuePostedEvenWhenNull() { - val live = settingsManager.getBooleanLive(SETTING_TEST).map { value -> - value - } - assertNull(live.getOrAwaitValue()) - - // posts value to main thread, InstantTaskExecutorRule is required to execute it instantly - settingsManager.putBoolean(SETTING_TEST, true) - runBlocking(Dispatchers.Main) { // observeForever can't be run in background thread - assertTrue(live.getOrAwaitValue()!!) + fun test_observerFlow_initialValue() = runBlocking { + var counter = 0 + val live = settingsManager.observerFlow { + if (counter++ == 0) + 23 + else + throw AssertionError("A second value was requested") } + assertEquals(23, live.first()) } @Test - fun test_getBooleanLive_getValue() { - val live = settingsManager.getBooleanLive(SETTING_TEST) - assertNull(live.value) - - // posts value to main thread, InstantTaskExecutorRule is required to execute it instantly - settingsManager.putBoolean(SETTING_TEST, true) - runBlocking(Dispatchers.Main) { // observeForever can't be run in background thread - assertTrue(live.getOrAwaitValue()!!) - } - } - - - @Test - fun test_ObserverCalledWhenValueChanges() { - val value = CompletableDeferred() - val observer = SettingsManager.OnChangeListener { - value.complete(settingsManager.getInt(SETTING_TEST)) - } - - try { - settingsManager.addOnChangeListener(observer) - settingsManager.putInt(SETTING_TEST, 123) - - runBlocking { - // wait until observer is called - assertEquals(123, value.await()) + fun test_observerFlow_updatedValue() = runBlocking { + var counter = 0 + val live = settingsManager.observerFlow { + when (counter++) { + 0 -> { + // update some setting so that we will be called a second time + settingsManager.putBoolean(SETTING_TEST, true) + // and emit initial value + 23 + } + 1 -> 42 // updated value + else -> throw AssertionError() } - - } finally { - settingsManager.removeOnChangeListener(observer) } + + val result = live.take(2).toList() + assertEquals(listOf(23, 42), result) } } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsManager.kt index 3ed64336..89c7ab17 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsManager.kt @@ -7,8 +7,9 @@ package at.bitfire.davdroid.settings import android.content.Context import android.util.NoSuchPropertyException import androidx.annotation.AnyThread -import androidx.lifecycle.LiveData +import androidx.annotation.VisibleForTesting import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.settings.SettingsManager.OnChangeListener import dagger.Module import dagger.Provides import dagger.hilt.EntryPoint @@ -16,6 +17,9 @@ import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow import java.io.Writer import java.lang.ref.WeakReference import java.util.LinkedList @@ -100,11 +104,34 @@ class SettingsManager internal constructor( } } + /** + * Returns a Flow that + * + * - always emits the initial value of the setting, and then + * - emits the new value whenever the setting changes. + * + * @param getValue used to determine the current value of the setting + */ + @VisibleForTesting + internal fun observerFlow(getValue: () -> T): Flow = callbackFlow { + // emit value on changes + val listener = OnChangeListener { + trySend(getValue()) + } + addOnChangeListener(listener) + + // get current value and emit it as first state + trySend(getValue()) + + // wait and clean up + awaitClose { removeOnChangeListener(listener) } + } + /*** SETTINGS ACCESS ***/ fun containsKey(key: String) = providers.any { it.contains(key) } - fun containsKeyLive(key: String) = SettingLiveData { containsKey(key) } + fun containsKeyFlow(key: String): Flow = observerFlow { containsKey(key) } private fun getValue(key: String, reader: (SettingsProvider) -> T?): T? { Logger.log.fine("Looking up setting $key") @@ -126,17 +153,17 @@ class SettingsManager internal constructor( fun getBooleanOrNull(key: String): Boolean? = getValue(key) { provider -> provider.getBoolean(key) } fun getBoolean(key: String): Boolean = getBooleanOrNull(key) ?: throw NoSuchPropertyException(key) - fun getBooleanLive(key: String): LiveData = SettingLiveData { getBooleanOrNull(key) } + fun getBooleanFlow(key: String): Flow = observerFlow { getBooleanOrNull(key) } fun getIntOrNull(key: String): Int? = getValue(key) { provider -> provider.getInt(key) } fun getInt(key: String): Int = getIntOrNull(key) ?: throw NoSuchPropertyException(key) - fun getIntLive(key: String): LiveData = SettingLiveData { getIntOrNull(key) } + fun getIntFlow(key: String): Flow = observerFlow { getIntOrNull(key) } fun getLongOrNull(key: String): Long? = getValue(key) { provider -> provider.getLong(key) } fun getLong(key: String) = getLongOrNull(key) ?: throw NoSuchPropertyException(key) fun getString(key: String) = getValue(key) { provider -> provider.getString(key) } - fun getStringLive(key: String): LiveData = SettingLiveData { getString(key) } + fun getStringFlow(key: String): Flow = observerFlow { getString(key) } fun isWritable(key: String): Boolean { @@ -175,33 +202,6 @@ class SettingsManager internal constructor( fun remove(key: String) = putString(key, null) - inner class SettingLiveData( - val getValueOrNull: () -> T? - ): LiveData(), OnChangeListener { - private var hasValue = false - - override fun onActive() { - addOnChangeListener(this) - update() - } - - override fun onInactive() { - removeOnChangeListener(this) - } - - override fun onSettingsChanged() { - update() - } - - @Synchronized - private fun update() { - val newValue = getValueOrNull() - if (!hasValue || value != newValue) - postValue(newValue) - } - } - - /*** HELPERS ***/ fun dump(writer: Writer) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt index 21e6bec2..88beb5fd 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt @@ -57,6 +57,8 @@ import androidx.core.content.getSystemService import androidx.core.graphics.drawable.toBitmap import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.compose.viewModel import androidx.preference.PreferenceManager import at.bitfire.cert4android.CustomCertStore @@ -149,16 +151,16 @@ class AppSettingsActivity: AppCompatActivity() { ) AppSettings_Connection( - proxyType = model.settings.getIntLive(Settings.PROXY_TYPE).observeAsState().value ?: Settings.PROXY_TYPE_NONE, + proxyType = model.settings.getIntFlow(Settings.PROXY_TYPE).collectAsStateWithLifecycle(null).value ?: Settings.PROXY_TYPE_NONE, onProxyTypeUpdated = { model.settings.putInt(Settings.PROXY_TYPE, it) }, - proxyHostName = model.settings.getStringLive(Settings.PROXY_HOST).observeAsState(null).value, + proxyHostName = model.settings.getStringFlow(Settings.PROXY_HOST).collectAsStateWithLifecycle(null).value, onProxyHostNameUpdated = { model.settings.putString(Settings.PROXY_HOST, it) }, - proxyPort = model.settings.getIntLive(Settings.PROXY_PORT).observeAsState(null).value, + proxyPort = model.settings.getIntFlow(Settings.PROXY_PORT).collectAsStateWithLifecycle(null).value, onProxyPortUpdated = { model.settings.putInt(Settings.PROXY_PORT, it) } ) AppSettings_Security( - distrustSystemCerts = model.settings.getBooleanLive(Settings.DISTRUST_SYSTEM_CERTIFICATES).observeAsState().value ?: false, + distrustSystemCerts = model.settings.getBooleanFlow(Settings.DISTRUST_SYSTEM_CERTIFICATES).collectAsStateWithLifecycle(null).value ?: false, onDistrustSystemCertsUpdated = { model.settings.putBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES, it) }, onResetCertificates = { model.resetCertificates() @@ -170,7 +172,7 @@ class AppSettingsActivity: AppCompatActivity() { ) AppSettings_UserInterface( - theme = model.settings.getIntLive(Settings.PREFERRED_THEME).observeAsState().value ?: Settings.PREFERRED_THEME_DEFAULT, + theme = model.settings.getIntFlow(Settings.PREFERRED_THEME).collectAsStateWithLifecycle(null).value ?: Settings.PREFERRED_THEME_DEFAULT, onThemeSelected = { model.settings.putInt(Settings.PREFERRED_THEME, it) UiUtils.updateTheme(context) @@ -184,7 +186,7 @@ class AppSettingsActivity: AppCompatActivity() { ) AppSettings_Integration( - taskProvider = TaskUtils.currentProviderLive(context).observeAsState().value + taskProvider = TaskUtils.currentProviderFlow(context, lifecycleScope).collectAsStateWithLifecycle().value ) } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt index 2108e9b4..7cd58a15 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt @@ -41,6 +41,8 @@ import androidx.compose.ui.unit.dp import androidx.core.text.HtmlCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel @@ -103,7 +105,7 @@ class TasksActivity: AppCompatActivity() { } - val showAgain = settings.getBooleanLive(HINT_OPENTASKS_NOT_INSTALLED) + val showAgain = settings.getBooleanFlow(HINT_OPENTASKS_NOT_INSTALLED) fun setShowAgain(showAgain: Boolean) { if (showAgain) settings.remove(HINT_OPENTASKS_NOT_INSTALLED) @@ -111,7 +113,7 @@ class TasksActivity: AppCompatActivity() { settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false) } - val currentProvider = TaskUtils.currentProviderLive(context) + val currentProvider = TaskUtils.currentProviderFlow(context, viewModelScope).asLiveData() val jtxSelected = currentProvider.map { it == TaskProvider.ProviderName.JtxBoard } val tasksOrgSelected = currentProvider.map { it == TaskProvider.ProviderName.TasksOrg } val openTasksSelected = currentProvider.map { it == TaskProvider.ProviderName.OpenTasks } @@ -171,7 +173,7 @@ fun TasksCard( val openTasksInstalled by model.openTasksInstalled.observeAsState(false) val openTasksSelected by model.openTasksSelected.observeAsState(false) - val showAgain = model.showAgain.observeAsState().value ?: true + val showAgain = model.showAgain.collectAsStateWithLifecycle(null).value ?: false fun installApp(packageName: String) { val uri = Uri.parse("market://details?id=$packageName&referrer=" + diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountModel.kt index 0d707854..1e3a62d4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountModel.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData import androidx.lifecycle.map import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope @@ -134,7 +135,7 @@ class AccountModel @AssistedInject constructor( ) val addressBooksPager = CollectionPager(db, cardDavSvc, Collection.TYPE_ADDRESSBOOK, showOnlyPersonal) - private val tasksProvider = TaskUtils.currentProviderLive(context) + private val tasksProvider = TaskUtils.currentProviderFlow(context, viewModelScope) val calDavSvc = db.serviceDao().getLiveByAccountAndType(account.name, Service.TYPE_CALDAV) val bindableCalendarHomesets = calDavSvc.switchMap { svc -> if (svc != null) @@ -150,7 +151,7 @@ class AccountModel @AssistedInject constructor( return@switchMap null RefreshCollectionsWorker.exists(context, RefreshCollectionsWorker.workerName(svc.id)) } - val calDavSyncPending = tasksProvider.switchMap { tasks -> + val calDavSyncPending = tasksProvider.asLiveData().switchMap { tasks -> BaseSyncWorker.exists( context, listOf(WorkInfo.State.ENQUEUED), @@ -162,7 +163,7 @@ class AccountModel @AssistedInject constructor( } ) } - val calDavSyncing = tasksProvider.switchMap { tasks -> + val calDavSyncing = tasksProvider.asLiveData().switchMap { tasks -> BaseSyncWorker.exists( context, listOf(WorkInfo.State.RUNNING), diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt index a9c03cca..74571b10 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import androidx.core.content.getSystemService import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.Constants @@ -102,7 +103,7 @@ class BatteryOptimizationsPage: IntroPage { model.checkBatteryOptimizations() } - val hintBatteryOptimizations by model.hintBatteryOptimizations.observeAsState() + val hintBatteryOptimizations by model.hintBatteryOptimizations.collectAsStateWithLifecycle(false) val shouldBeExempted by model.shouldBeExempted.observeAsState(false) val isExempted by model.isExempted.observeAsState(false) LaunchedEffect(shouldBeExempted, isExempted) { @@ -110,7 +111,7 @@ class BatteryOptimizationsPage: IntroPage { ignoreBatteryOptimizationsResultLauncher.launch(BuildConfig.APPLICATION_ID) } - val hintAutostartPermission by model.hintAutostartPermission.observeAsState() + val hintAutostartPermission by model.hintAutostartPermission.collectAsStateWithLifecycle(false) BatteryOptimizationsContent( dontShowBattery = hintBatteryOptimizations == false, onChangeDontShowBattery = { @@ -178,14 +179,14 @@ class BatteryOptimizationsPage: IntroPage { val shouldBeExempted = MutableLiveData() val isExempted = MutableLiveData() - val hintBatteryOptimizations = settings.getBooleanLive(HINT_BATTERY_OPTIMIZATIONS) + val hintBatteryOptimizations = settings.getBooleanFlow(HINT_BATTERY_OPTIMIZATIONS) private val batteryOptimizationsReceiver = object: BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { checkBatteryOptimizations() } } - val hintAutostartPermission = settings.getBooleanLive(HINT_AUTOSTART_PERMISSION) + val hintAutostartPermission = settings.getBooleanFlow(HINT_AUTOSTART_PERMISSION) init { val intentFilter = IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt index 996526b0..4ebeb204 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt @@ -21,7 +21,6 @@ import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -31,6 +30,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.davdroid.Constants import at.bitfire.davdroid.Constants.withStatParams @@ -68,7 +68,7 @@ class OpenSourcePage : IntroPage { @Composable private fun Page(model: Model = viewModel()) { - val dontShow by model.dontShow.observeAsState(false) + val dontShow by model.dontShow.collectAsStateWithLifecycle(false) PageContent( dontShow = dontShow, onChangeDontShow = { @@ -147,7 +147,8 @@ class OpenSourcePage : IntroPage { const val SETTING_NEXT_DONATION_POPUP = "time_nextDonationPopup" } - val dontShow = settings.containsKeyLive(SETTING_NEXT_DONATION_POPUP) + val dontShow = settings.containsKeyFlow(SETTING_NEXT_DONATION_POPUP) + fun setDontShow(dontShowAgain: Boolean) { if (dontShowAgain) { val nextReminder = System.currentTimeMillis() + 90*86400000L // 90 days (~ 3 months) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt index a9d50516..c4d4c832 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.davdroid.R import at.bitfire.davdroid.servicedetection.DavResourceFinder @@ -96,8 +97,9 @@ fun AccountDetailsPage( val suggestedAccountNames = foundConfig.calDAV?.emails ?: emptyList() var accountName by remember { mutableStateOf(suggestedAccountNames.firstOrNull() ?: "") } - val forcedGroupMethod by model.forcedGroupMethod.observeAsState() - var groupMethod by remember { mutableStateOf(forcedGroupMethod ?: loginInfo.suggestedGroupMethod) } + var groupMethod by remember { mutableStateOf(loginInfo.suggestedGroupMethod) } + val forcedGroupMethod by model.forcedGroupMethod.collectAsStateWithLifecycle(null) + forcedGroupMethod?.let { groupMethod = it } AccountDetailsPage_Content( suggestedAccountNames = suggestedAccountNames, accountName = accountName, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt index dca2d483..7660e924 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt @@ -14,7 +14,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData -import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R @@ -33,6 +32,7 @@ import at.bitfire.davdroid.util.TaskUtils import at.bitfire.vcard4android.GroupMethod import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible import java.util.logging.Level @@ -45,7 +45,7 @@ class LoginModel @Inject constructor( val settingsManager: SettingsManager ): ViewModel() { - val forcedGroupMethod = settingsManager.getStringLive(AccountSettings.KEY_CONTACT_GROUP_METHOD).map { methodName -> + val forcedGroupMethod = settingsManager.getStringFlow(AccountSettings.KEY_CONTACT_GROUP_METHOD).map { methodName -> methodName?.let { try { GroupMethod.valueOf(it) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/TaskUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/TaskUtils.kt index 0a5602a4..068dc276 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/TaskUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/TaskUtils.kt @@ -15,8 +15,6 @@ import android.graphics.drawable.BitmapDrawable import android.net.Uri import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.lifecycle.LiveData -import androidx.lifecycle.map import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Service @@ -34,6 +32,11 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn object TaskUtils { @@ -57,16 +60,16 @@ object TaskUtils { /** * Returns the currently selected tasks provider (if it's still available = installed). * - * @return the currently selected tasks provider, or null if none is available + * @return flow with the currently selected tasks provider */ - fun currentProviderLive(context: Context): LiveData { + fun currentProviderFlow(context: Context, externalScope: CoroutineScope): StateFlow { val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() - return settingsManager.getStringLive(Settings.SELECTED_TASKS_PROVIDER).map { preferred -> + return settingsManager.getStringFlow(Settings.SELECTED_TASKS_PROVIDER).map { preferred -> if (preferred != null) preferredAuthorityToProviderName(preferred, context.packageManager) else null - } + }.stateIn(scope = externalScope, started = SharingStarted.WhileSubscribed(), initialValue = null) } private fun preferredAuthorityToProviderName( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96e92825..9f812b4d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ androidx-constraintLayout = "2.1.4" androidx-core = "1.12.0" androidx-fragment = "1.6.2" androidx-hilt = "1.2.0" +androidx-lifecycle = "2.7.0" androidx-paging = "3.2.1" androidx-preference = "1.2.1" androidx-security = "1.1.0-alpha06" @@ -23,7 +24,6 @@ androidx-test-core = "1.5.0" androidx-test-runner = "1.5.2" androidx-test-rules = "1.5.0" androidx-test-junit = "1.1.5" -androidx-viewmodel = "2.7.0" androidx-work = "2.9.0" appIntro = "7.0.0-beta02" bitfire-cert4android = "f1cc9b9ca3" @@ -72,8 +72,9 @@ androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-cor androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidx-hilt" } androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androidx-hilt" } -androidx-lifecycle-viewmodel-base = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-viewmodel" } -androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-viewmodel" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewmodel-base = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-paging = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidx-paging" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" } androidx-preference = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" }