mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-21 10:41:47 +00:00
SettingsManager: use Flows instead of LiveData (#714)
* SettingsManager: use flows instead of LiveData * Fix tests
This commit is contained in:
parent
1cd0df1e6a
commit
b88c35169e
|
@ -149,6 +149,7 @@ dependencies {
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.fragment)
|
implementation(libs.androidx.fragment)
|
||||||
implementation(libs.androidx.hilt.work)
|
implementation(libs.androidx.hilt.work)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.base)
|
implementation(libs.androidx.lifecycle.viewmodel.base)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
implementation(libs.androidx.paging)
|
implementation(libs.androidx.paging)
|
||||||
|
|
|
@ -5,18 +5,15 @@
|
||||||
package at.bitfire.davdroid.settings
|
package at.bitfire.davdroid.settings
|
||||||
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
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.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.flow.take
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -62,51 +59,35 @@ class SettingsManagerTest {
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_getBooleanLive_initialValuePostedEvenWhenNull() {
|
fun test_observerFlow_initialValue() = runBlocking {
|
||||||
val live = settingsManager.getBooleanLive(SETTING_TEST).map { value ->
|
var counter = 0
|
||||||
value
|
val live = settingsManager.observerFlow {
|
||||||
}
|
if (counter++ == 0)
|
||||||
assertNull(live.getOrAwaitValue())
|
23
|
||||||
|
else
|
||||||
// posts value to main thread, InstantTaskExecutorRule is required to execute it instantly
|
throw AssertionError("A second value was requested")
|
||||||
settingsManager.putBoolean(SETTING_TEST, true)
|
|
||||||
runBlocking(Dispatchers.Main) { // observeForever can't be run in background thread
|
|
||||||
assertTrue(live.getOrAwaitValue()!!)
|
|
||||||
}
|
}
|
||||||
|
assertEquals(23, live.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_getBooleanLive_getValue() {
|
fun test_observerFlow_updatedValue() = runBlocking {
|
||||||
val live = settingsManager.getBooleanLive(SETTING_TEST)
|
var counter = 0
|
||||||
assertNull(live.value)
|
val live = settingsManager.observerFlow {
|
||||||
|
when (counter++) {
|
||||||
// posts value to main thread, InstantTaskExecutorRule is required to execute it instantly
|
0 -> {
|
||||||
settingsManager.putBoolean(SETTING_TEST, true)
|
// update some setting so that we will be called a second time
|
||||||
runBlocking(Dispatchers.Main) { // observeForever can't be run in background thread
|
settingsManager.putBoolean(SETTING_TEST, true)
|
||||||
assertTrue(live.getOrAwaitValue()!!)
|
// and emit initial value
|
||||||
}
|
23
|
||||||
}
|
}
|
||||||
|
1 -> 42 // updated value
|
||||||
|
else -> throw AssertionError()
|
||||||
@Test
|
|
||||||
fun test_ObserverCalledWhenValueChanges() {
|
|
||||||
val value = CompletableDeferred<Int>()
|
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
|
||||||
settingsManager.removeOnChangeListener(observer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val result = live.take(2).toList()
|
||||||
|
assertEquals(listOf(23, 42), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,8 +7,9 @@ package at.bitfire.davdroid.settings
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.NoSuchPropertyException
|
import android.util.NoSuchPropertyException
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.annotation.VisibleForTesting
|
||||||
import at.bitfire.davdroid.log.Logger
|
import at.bitfire.davdroid.log.Logger
|
||||||
|
import at.bitfire.davdroid.settings.SettingsManager.OnChangeListener
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.EntryPoint
|
import dagger.hilt.EntryPoint
|
||||||
|
@ -16,6 +17,9 @@ import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
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.io.Writer
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.LinkedList
|
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<T> observerFlow(getValue: () -> T): Flow<T> = 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 ***/
|
/*** SETTINGS ACCESS ***/
|
||||||
|
|
||||||
fun containsKey(key: String) = providers.any { it.contains(key) }
|
fun containsKey(key: String) = providers.any { it.contains(key) }
|
||||||
fun containsKeyLive(key: String) = SettingLiveData { containsKey(key) }
|
fun containsKeyFlow(key: String): Flow<Boolean> = observerFlow { containsKey(key) }
|
||||||
|
|
||||||
private fun<T> getValue(key: String, reader: (SettingsProvider) -> T?): T? {
|
private fun<T> getValue(key: String, reader: (SettingsProvider) -> T?): T? {
|
||||||
Logger.log.fine("Looking up setting $key")
|
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 getBooleanOrNull(key: String): Boolean? = getValue(key) { provider -> provider.getBoolean(key) }
|
||||||
fun getBoolean(key: String): Boolean = getBooleanOrNull(key) ?: throw NoSuchPropertyException(key)
|
fun getBoolean(key: String): Boolean = getBooleanOrNull(key) ?: throw NoSuchPropertyException(key)
|
||||||
fun getBooleanLive(key: String): LiveData<Boolean?> = SettingLiveData { getBooleanOrNull(key) }
|
fun getBooleanFlow(key: String): Flow<Boolean?> = observerFlow { getBooleanOrNull(key) }
|
||||||
|
|
||||||
fun getIntOrNull(key: String): Int? = getValue(key) { provider -> provider.getInt(key) }
|
fun getIntOrNull(key: String): Int? = getValue(key) { provider -> provider.getInt(key) }
|
||||||
fun getInt(key: String): Int = getIntOrNull(key) ?: throw NoSuchPropertyException(key)
|
fun getInt(key: String): Int = getIntOrNull(key) ?: throw NoSuchPropertyException(key)
|
||||||
fun getIntLive(key: String): LiveData<Int?> = SettingLiveData { getIntOrNull(key) }
|
fun getIntFlow(key: String): Flow<Int?> = observerFlow { getIntOrNull(key) }
|
||||||
|
|
||||||
fun getLongOrNull(key: String): Long? = getValue(key) { provider -> provider.getLong(key) }
|
fun getLongOrNull(key: String): Long? = getValue(key) { provider -> provider.getLong(key) }
|
||||||
fun getLong(key: String) = getLongOrNull(key) ?: throw NoSuchPropertyException(key)
|
fun getLong(key: String) = getLongOrNull(key) ?: throw NoSuchPropertyException(key)
|
||||||
|
|
||||||
fun getString(key: String) = getValue(key) { provider -> provider.getString(key) }
|
fun getString(key: String) = getValue(key) { provider -> provider.getString(key) }
|
||||||
fun getStringLive(key: String): LiveData<String?> = SettingLiveData { getString(key) }
|
fun getStringFlow(key: String): Flow<String?> = observerFlow { getString(key) }
|
||||||
|
|
||||||
|
|
||||||
fun isWritable(key: String): Boolean {
|
fun isWritable(key: String): Boolean {
|
||||||
|
@ -175,33 +202,6 @@ class SettingsManager internal constructor(
|
||||||
fun remove(key: String) = putString(key, null)
|
fun remove(key: String) = putString(key, null)
|
||||||
|
|
||||||
|
|
||||||
inner class SettingLiveData<T>(
|
|
||||||
val getValueOrNull: () -> T?
|
|
||||||
): LiveData<T>(), 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 ***/
|
/*** HELPERS ***/
|
||||||
|
|
||||||
fun dump(writer: Writer) {
|
fun dump(writer: Writer) {
|
||||||
|
|
|
@ -57,6 +57,8 @@ import androidx.core.content.getSystemService
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import at.bitfire.cert4android.CustomCertStore
|
import at.bitfire.cert4android.CustomCertStore
|
||||||
|
@ -149,16 +151,16 @@ class AppSettingsActivity: AppCompatActivity() {
|
||||||
)
|
)
|
||||||
|
|
||||||
AppSettings_Connection(
|
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) },
|
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) },
|
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) }
|
onProxyPortUpdated = { model.settings.putInt(Settings.PROXY_PORT, it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
AppSettings_Security(
|
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) },
|
onDistrustSystemCertsUpdated = { model.settings.putBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES, it) },
|
||||||
onResetCertificates = {
|
onResetCertificates = {
|
||||||
model.resetCertificates()
|
model.resetCertificates()
|
||||||
|
@ -170,7 +172,7 @@ class AppSettingsActivity: AppCompatActivity() {
|
||||||
)
|
)
|
||||||
|
|
||||||
AppSettings_UserInterface(
|
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 = {
|
onThemeSelected = {
|
||||||
model.settings.putInt(Settings.PREFERRED_THEME, it)
|
model.settings.putInt(Settings.PREFERRED_THEME, it)
|
||||||
UiUtils.updateTheme(context)
|
UiUtils.updateTheme(context)
|
||||||
|
@ -184,7 +186,7 @@ class AppSettingsActivity: AppCompatActivity() {
|
||||||
)
|
)
|
||||||
|
|
||||||
AppSettings_Integration(
|
AppSettings_Integration(
|
||||||
taskProvider = TaskUtils.currentProviderLive(context).observeAsState().value
|
taskProvider = TaskUtils.currentProviderFlow(context, lifecycleScope).collectAsStateWithLifecycle().value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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) {
|
fun setShowAgain(showAgain: Boolean) {
|
||||||
if (showAgain)
|
if (showAgain)
|
||||||
settings.remove(HINT_OPENTASKS_NOT_INSTALLED)
|
settings.remove(HINT_OPENTASKS_NOT_INSTALLED)
|
||||||
|
@ -111,7 +113,7 @@ class TasksActivity: AppCompatActivity() {
|
||||||
settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false)
|
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 jtxSelected = currentProvider.map { it == TaskProvider.ProviderName.JtxBoard }
|
||||||
val tasksOrgSelected = currentProvider.map { it == TaskProvider.ProviderName.TasksOrg }
|
val tasksOrgSelected = currentProvider.map { it == TaskProvider.ProviderName.TasksOrg }
|
||||||
val openTasksSelected = currentProvider.map { it == TaskProvider.ProviderName.OpenTasks }
|
val openTasksSelected = currentProvider.map { it == TaskProvider.ProviderName.OpenTasks }
|
||||||
|
@ -171,7 +173,7 @@ fun TasksCard(
|
||||||
val openTasksInstalled by model.openTasksInstalled.observeAsState(false)
|
val openTasksInstalled by model.openTasksInstalled.observeAsState(false)
|
||||||
val openTasksSelected by model.openTasksSelected.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) {
|
fun installApp(packageName: String) {
|
||||||
val uri = Uri.parse("market://details?id=$packageName&referrer=" +
|
val uri = Uri.parse("market://details?id=$packageName&referrer=" +
|
||||||
|
|
|
@ -23,6 +23,7 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.switchMap
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -134,7 +135,7 @@ class AccountModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
val addressBooksPager = CollectionPager(db, cardDavSvc, Collection.TYPE_ADDRESSBOOK, showOnlyPersonal)
|
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 calDavSvc = db.serviceDao().getLiveByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||||
val bindableCalendarHomesets = calDavSvc.switchMap { svc ->
|
val bindableCalendarHomesets = calDavSvc.switchMap { svc ->
|
||||||
if (svc != null)
|
if (svc != null)
|
||||||
|
@ -150,7 +151,7 @@ class AccountModel @AssistedInject constructor(
|
||||||
return@switchMap null
|
return@switchMap null
|
||||||
RefreshCollectionsWorker.exists(context, RefreshCollectionsWorker.workerName(svc.id))
|
RefreshCollectionsWorker.exists(context, RefreshCollectionsWorker.workerName(svc.id))
|
||||||
}
|
}
|
||||||
val calDavSyncPending = tasksProvider.switchMap { tasks ->
|
val calDavSyncPending = tasksProvider.asLiveData().switchMap { tasks ->
|
||||||
BaseSyncWorker.exists(
|
BaseSyncWorker.exists(
|
||||||
context,
|
context,
|
||||||
listOf(WorkInfo.State.ENQUEUED),
|
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(
|
BaseSyncWorker.exists(
|
||||||
context,
|
context,
|
||||||
listOf(WorkInfo.State.RUNNING),
|
listOf(WorkInfo.State.RUNNING),
|
||||||
|
|
|
@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import at.bitfire.davdroid.BuildConfig
|
import at.bitfire.davdroid.BuildConfig
|
||||||
import at.bitfire.davdroid.Constants
|
import at.bitfire.davdroid.Constants
|
||||||
|
@ -102,7 +103,7 @@ class BatteryOptimizationsPage: IntroPage {
|
||||||
model.checkBatteryOptimizations()
|
model.checkBatteryOptimizations()
|
||||||
}
|
}
|
||||||
|
|
||||||
val hintBatteryOptimizations by model.hintBatteryOptimizations.observeAsState()
|
val hintBatteryOptimizations by model.hintBatteryOptimizations.collectAsStateWithLifecycle(false)
|
||||||
val shouldBeExempted by model.shouldBeExempted.observeAsState(false)
|
val shouldBeExempted by model.shouldBeExempted.observeAsState(false)
|
||||||
val isExempted by model.isExempted.observeAsState(false)
|
val isExempted by model.isExempted.observeAsState(false)
|
||||||
LaunchedEffect(shouldBeExempted, isExempted) {
|
LaunchedEffect(shouldBeExempted, isExempted) {
|
||||||
|
@ -110,7 +111,7 @@ class BatteryOptimizationsPage: IntroPage {
|
||||||
ignoreBatteryOptimizationsResultLauncher.launch(BuildConfig.APPLICATION_ID)
|
ignoreBatteryOptimizationsResultLauncher.launch(BuildConfig.APPLICATION_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
val hintAutostartPermission by model.hintAutostartPermission.observeAsState()
|
val hintAutostartPermission by model.hintAutostartPermission.collectAsStateWithLifecycle(false)
|
||||||
BatteryOptimizationsContent(
|
BatteryOptimizationsContent(
|
||||||
dontShowBattery = hintBatteryOptimizations == false,
|
dontShowBattery = hintBatteryOptimizations == false,
|
||||||
onChangeDontShowBattery = {
|
onChangeDontShowBattery = {
|
||||||
|
@ -178,14 +179,14 @@ class BatteryOptimizationsPage: IntroPage {
|
||||||
|
|
||||||
val shouldBeExempted = MutableLiveData<Boolean>()
|
val shouldBeExempted = MutableLiveData<Boolean>()
|
||||||
val isExempted = MutableLiveData<Boolean>()
|
val isExempted = MutableLiveData<Boolean>()
|
||||||
val hintBatteryOptimizations = settings.getBooleanLive(HINT_BATTERY_OPTIMIZATIONS)
|
val hintBatteryOptimizations = settings.getBooleanFlow(HINT_BATTERY_OPTIMIZATIONS)
|
||||||
private val batteryOptimizationsReceiver = object: BroadcastReceiver() {
|
private val batteryOptimizationsReceiver = object: BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
checkBatteryOptimizations()
|
checkBatteryOptimizations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val hintAutostartPermission = settings.getBooleanLive(HINT_AUTOSTART_PERMISSION)
|
val hintAutostartPermission = settings.getBooleanFlow(HINT_AUTOSTART_PERMISSION)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val intentFilter = IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED)
|
val intentFilter = IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED)
|
||||||
|
|
|
@ -21,7 +21,6 @@ import androidx.compose.material.OutlinedButton
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import at.bitfire.davdroid.Constants
|
import at.bitfire.davdroid.Constants
|
||||||
import at.bitfire.davdroid.Constants.withStatParams
|
import at.bitfire.davdroid.Constants.withStatParams
|
||||||
|
@ -68,7 +68,7 @@ class OpenSourcePage : IntroPage {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Page(model: Model = viewModel()) {
|
private fun Page(model: Model = viewModel()) {
|
||||||
val dontShow by model.dontShow.observeAsState(false)
|
val dontShow by model.dontShow.collectAsStateWithLifecycle(false)
|
||||||
PageContent(
|
PageContent(
|
||||||
dontShow = dontShow,
|
dontShow = dontShow,
|
||||||
onChangeDontShow = {
|
onChangeDontShow = {
|
||||||
|
@ -147,7 +147,8 @@ class OpenSourcePage : IntroPage {
|
||||||
const val SETTING_NEXT_DONATION_POPUP = "time_nextDonationPopup"
|
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) {
|
fun setDontShow(dontShowAgain: Boolean) {
|
||||||
if (dontShowAgain) {
|
if (dontShowAgain) {
|
||||||
val nextReminder = System.currentTimeMillis() + 90*86400000L // 90 days (~ 3 months)
|
val nextReminder = System.currentTimeMillis() + 90*86400000L // 90 days (~ 3 months)
|
||||||
|
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import at.bitfire.davdroid.R
|
import at.bitfire.davdroid.R
|
||||||
import at.bitfire.davdroid.servicedetection.DavResourceFinder
|
import at.bitfire.davdroid.servicedetection.DavResourceFinder
|
||||||
|
@ -96,8 +97,9 @@ fun AccountDetailsPage(
|
||||||
val suggestedAccountNames = foundConfig.calDAV?.emails ?: emptyList()
|
val suggestedAccountNames = foundConfig.calDAV?.emails ?: emptyList()
|
||||||
var accountName by remember { mutableStateOf(suggestedAccountNames.firstOrNull() ?: "") }
|
var accountName by remember { mutableStateOf(suggestedAccountNames.firstOrNull() ?: "") }
|
||||||
|
|
||||||
val forcedGroupMethod by model.forcedGroupMethod.observeAsState()
|
var groupMethod by remember { mutableStateOf(loginInfo.suggestedGroupMethod) }
|
||||||
var groupMethod by remember { mutableStateOf(forcedGroupMethod ?: loginInfo.suggestedGroupMethod) }
|
val forcedGroupMethod by model.forcedGroupMethod.collectAsStateWithLifecycle(null)
|
||||||
|
forcedGroupMethod?.let { groupMethod = it }
|
||||||
AccountDetailsPage_Content(
|
AccountDetailsPage_Content(
|
||||||
suggestedAccountNames = suggestedAccountNames,
|
suggestedAccountNames = suggestedAccountNames,
|
||||||
accountName = accountName,
|
accountName = accountName,
|
||||||
|
|
|
@ -14,7 +14,6 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import androidx.lifecycle.map
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.bitfire.davdroid.InvalidAccountException
|
import at.bitfire.davdroid.InvalidAccountException
|
||||||
import at.bitfire.davdroid.R
|
import at.bitfire.davdroid.R
|
||||||
|
@ -33,6 +32,7 @@ import at.bitfire.davdroid.util.TaskUtils
|
||||||
import at.bitfire.vcard4android.GroupMethod
|
import at.bitfire.vcard4android.GroupMethod
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
@ -45,7 +45,7 @@ class LoginModel @Inject constructor(
|
||||||
val settingsManager: SettingsManager
|
val settingsManager: SettingsManager
|
||||||
): ViewModel() {
|
): 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 {
|
methodName?.let {
|
||||||
try {
|
try {
|
||||||
GroupMethod.valueOf(it)
|
GroupMethod.valueOf(it)
|
||||||
|
|
|
@ -15,8 +15,6 @@ import android.graphics.drawable.BitmapDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.map
|
|
||||||
import at.bitfire.davdroid.InvalidAccountException
|
import at.bitfire.davdroid.InvalidAccountException
|
||||||
import at.bitfire.davdroid.R
|
import at.bitfire.davdroid.R
|
||||||
import at.bitfire.davdroid.db.Service
|
import at.bitfire.davdroid.db.Service
|
||||||
|
@ -34,6 +32,11 @@ import dagger.hilt.EntryPoint
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
import dagger.hilt.components.SingletonComponent
|
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 {
|
object TaskUtils {
|
||||||
|
|
||||||
|
@ -57,16 +60,16 @@ object TaskUtils {
|
||||||
/**
|
/**
|
||||||
* Returns the currently selected tasks provider (if it's still available = installed).
|
* 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<ProviderName?> {
|
fun currentProviderFlow(context: Context, externalScope: CoroutineScope): StateFlow<ProviderName?> {
|
||||||
val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager()
|
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)
|
if (preferred != null)
|
||||||
preferredAuthorityToProviderName(preferred, context.packageManager)
|
preferredAuthorityToProviderName(preferred, context.packageManager)
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
}
|
}.stateIn(scope = externalScope, started = SharingStarted.WhileSubscribed(), initialValue = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preferredAuthorityToProviderName(
|
private fun preferredAuthorityToProviderName(
|
||||||
|
|
|
@ -15,6 +15,7 @@ androidx-constraintLayout = "2.1.4"
|
||||||
androidx-core = "1.12.0"
|
androidx-core = "1.12.0"
|
||||||
androidx-fragment = "1.6.2"
|
androidx-fragment = "1.6.2"
|
||||||
androidx-hilt = "1.2.0"
|
androidx-hilt = "1.2.0"
|
||||||
|
androidx-lifecycle = "2.7.0"
|
||||||
androidx-paging = "3.2.1"
|
androidx-paging = "3.2.1"
|
||||||
androidx-preference = "1.2.1"
|
androidx-preference = "1.2.1"
|
||||||
androidx-security = "1.1.0-alpha06"
|
androidx-security = "1.1.0-alpha06"
|
||||||
|
@ -23,7 +24,6 @@ androidx-test-core = "1.5.0"
|
||||||
androidx-test-runner = "1.5.2"
|
androidx-test-runner = "1.5.2"
|
||||||
androidx-test-rules = "1.5.0"
|
androidx-test-rules = "1.5.0"
|
||||||
androidx-test-junit = "1.1.5"
|
androidx-test-junit = "1.1.5"
|
||||||
androidx-viewmodel = "2.7.0"
|
|
||||||
androidx-work = "2.9.0"
|
androidx-work = "2.9.0"
|
||||||
appIntro = "7.0.0-beta02"
|
appIntro = "7.0.0-beta02"
|
||||||
bitfire-cert4android = "f1cc9b9ca3"
|
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-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" }
|
||||||
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidx-hilt" }
|
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-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-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-viewmodel" }
|
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 = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidx-paging" }
|
||||||
androidx-paging-compose = { module = "androidx.paging:paging-compose", 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" }
|
androidx-preference = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" }
|
||||||
|
|
Loading…
Reference in a new issue