From 29f2b769ba66a0baaa6049abb8647deda1e8f47e Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 22 Aug 2025 17:06:53 +0200 Subject: [PATCH 1/8] EditSubscriptionScreen wrongly goes into edited state --- .../java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt index 38cb417a..a0f060f5 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt @@ -122,7 +122,7 @@ class EditSubscriptionModel @AssistedInject constructor( // Save state, before user makes changes initialSubscription = subscription initialCredential = credential - initialRequiresAuthValue = uiState.requiresAuth + initialRequiresAuthValue = requiresAuth } /** From da1225e59a52d5ded55762668384fba265fecbe9 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 26 Aug 2025 14:52:20 +0200 Subject: [PATCH 2/8] Set initial values as states --- .../java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt index a0f060f5..61803be3 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt @@ -40,9 +40,9 @@ class EditSubscriptionModel @AssistedInject constructor( fun create(subscriptionId: Long): EditSubscriptionModel } - private var initialSubscription: Subscription? = null - private var initialCredential: Credential? = null - private var initialRequiresAuthValue: Boolean? = null + private var initialSubscription: Subscription? by mutableStateOf(null) + private var initialCredential: Credential? by mutableStateOf(null) + private var initialRequiresAuthValue: Boolean? by mutableStateOf(null) /** * Whether user input is error free From 111ff7404b26ec9a5be61aa30d01b88cf7095399 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 26 Aug 2025 14:56:08 +0200 Subject: [PATCH 3/8] Move initial values storage --- .../icsdroid/model/EditSubscriptionModel.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt index 61803be3..8d6ce8ff 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt @@ -40,9 +40,9 @@ class EditSubscriptionModel @AssistedInject constructor( fun create(subscriptionId: Long): EditSubscriptionModel } - private var initialSubscription: Subscription? by mutableStateOf(null) - private var initialCredential: Credential? by mutableStateOf(null) - private var initialRequiresAuthValue: Boolean? by mutableStateOf(null) + private var initialSubscription: Subscription? = null + private var initialCredential: Credential? = null + private var initialRequiresAuthValue: Boolean? = null /** * Whether user input is error free @@ -101,6 +101,13 @@ class EditSubscriptionModel @AssistedInject constructor( private fun onSubscriptionLoaded(subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential) = with(subscriptionSettingsUseCase) { val subscription = subscriptionWithCredential.subscription + val credential = subscriptionWithCredential.credential + val requiresAuth = credential != null + + // Save the initial state, before updating the UI, so the state is persisted + initialSubscription = subscription + initialCredential = credential + initialRequiresAuthValue = requiresAuth setUrl(subscription.url.toString()) setTitle(subscription.displayName) @@ -110,19 +117,12 @@ class EditSubscriptionModel @AssistedInject constructor( setDefaultAllDayAlarmMinutes(subscription.defaultAllDayAlarmMinutes?.toString()) setIgnoreDescription(subscription.ignoreDescription) - val credential = subscriptionWithCredential.credential - val requiresAuth = credential != null setRequiresAuth(requiresAuth) if (credential != null) { setUsername(credential.username) setPassword(credential.password) } - - // Save state, before user makes changes - initialSubscription = subscription - initialCredential = credential - initialRequiresAuthValue = requiresAuth } /** From cd9335124c75d893b608d446fb5f9a9cde881c5e Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 26 Aug 2025 15:22:02 +0200 Subject: [PATCH 4/8] Fix --- .../icsdroid/model/EditSubscriptionModel.kt | 55 +++++-------------- .../model/SubscriptionSettingsUseCase.kt | 16 ++++++ .../ui/screen/EditSubscriptionScreen.kt | 14 ++++- 3 files changed, 41 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt index 8d6ce8ff..b66fdb5a 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt @@ -42,7 +42,7 @@ class EditSubscriptionModel @AssistedInject constructor( private var initialSubscription: Subscription? = null private var initialCredential: Credential? = null - private var initialRequiresAuthValue: Boolean? = null + private val initialRequiresAuth: Boolean? get() = initialCredential != null /** * Whether user input is error free @@ -69,7 +69,7 @@ class EditSubscriptionModel @AssistedInject constructor( get() = with(subscriptionSettingsUseCase) { val requiresAuth = uiState.requiresAuth - val credentialsDirty = initialRequiresAuthValue != requiresAuth || initialCredential?.let { + val credentialsDirty = initialRequiresAuth != requiresAuth || initialCredential?.let { !equalsCredential(it) } ?: false val subscriptionsDirty = initialSubscription?.let { @@ -82,55 +82,28 @@ class EditSubscriptionModel @AssistedInject constructor( var successMessage: String? by mutableStateOf(null) private set - val subscription = db.subscriptionsDao().getByIdFlow(subscriptionId) val subscriptionWithCredential = db.subscriptionsDao().getWithCredentialsByIdFlow(subscriptionId) - init { - // Initialise view models and save their initial state - viewModelScope.launch { - subscriptionWithCredential.collect { data -> - if (data != null) - onSubscriptionLoaded(data) - } - } - } - /** * Initialise view models and remember their initial state */ - private fun onSubscriptionLoaded(subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential) = - with(subscriptionSettingsUseCase) { - val subscription = subscriptionWithCredential.subscription - val credential = subscriptionWithCredential.credential - val requiresAuth = credential != null - - // Save the initial state, before updating the UI, so the state is persisted - initialSubscription = subscription - initialCredential = credential - initialRequiresAuthValue = requiresAuth - - setUrl(subscription.url.toString()) - setTitle(subscription.displayName) - setColor(subscription.color) - setIgnoreAlerts(subscription.ignoreEmbeddedAlerts) - setDefaultAlarmMinutes(subscription.defaultAlarmMinutes?.toString()) - setDefaultAllDayAlarmMinutes(subscription.defaultAllDayAlarmMinutes?.toString()) - setIgnoreDescription(subscription.ignoreDescription) - - setRequiresAuth(requiresAuth) - - if (credential != null) { - setUsername(credential.username) - setPassword(credential.password) - } - } + fun onSubscriptionLoaded(subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential) { + val subscription = subscriptionWithCredential.subscription + val credential = subscriptionWithCredential.credential + + // Save the initial state, before updating the UI, so the state is persisted + initialSubscription = subscription + initialCredential = credential + + subscriptionSettingsUseCase.update(subscription, credential) + } /** * Updates the loaded subscription from the data provided by the view models. */ fun updateSubscription() = with(subscriptionSettingsUseCase.uiState) { viewModelScope.launch(Dispatchers.IO) { - subscription.firstOrNull()?.let { subscription -> + subscriptionWithCredential.firstOrNull()?.let { (subscription) -> val newSubscription = subscription.copy( displayName = title ?: subscription.displayName, color = color, @@ -161,7 +134,7 @@ class EditSubscriptionModel @AssistedInject constructor( */ fun removeSubscription() { viewModelScope.launch(Dispatchers.IO) { - subscription.firstOrNull()?.let { subscription -> + subscriptionWithCredential.firstOrNull()?.let { (subscription) -> db.subscriptionsDao().delete(subscription) // sync the subscription to reflect the changes in the calendar provider diff --git a/app/src/main/java/at/bitfire/icsdroid/model/SubscriptionSettingsUseCase.kt b/app/src/main/java/at/bitfire/icsdroid/model/SubscriptionSettingsUseCase.kt index bf810f3f..dea90c6c 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/SubscriptionSettingsUseCase.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/SubscriptionSettingsUseCase.kt @@ -83,6 +83,22 @@ class SubscriptionSettingsUseCase @Inject constructor() { uiState = uiState.copy(ignoreDescription = value) } + fun update(subscription: Subscription, credential: Credential?) { + uiState = uiState.copy( + url = subscription.url.toString(), + title = subscription.displayName, + color = subscription.color, + ignoreAlerts = subscription.ignoreEmbeddedAlerts, + defaultAlarmMinutes = subscription.defaultAlarmMinutes, + defaultAllDayAlarmMinutes = subscription.defaultAllDayAlarmMinutes, + ignoreDescription = subscription.ignoreDescription, + + requiresAuth = credential != null, + username = credential?.username, + password = credential?.password + ) + } + fun equalsSubscription(subscription: Subscription) = uiState.url == subscription.url.toString() && uiState.title == subscription.displayName diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditSubscriptionScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditSubscriptionScreen.kt index c161ada0..3b4b440b 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditSubscriptionScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditSubscriptionScreen.kt @@ -22,6 +22,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -52,7 +55,12 @@ fun EditSubscriptionScreen( val model = hiltViewModel { factory -> factory.create(subscriptionId) } - val subscription = model.subscription.collectAsStateWithLifecycle(null) + val subscriptionWithCredential by model.subscriptionWithCredential.collectAsState(null) + + LaunchedEffect(subscriptionWithCredential) { + subscriptionWithCredential?.let { model.onSubscriptionLoaded(it) } + } + with(model.subscriptionSettingsUseCase) { EditSubscriptionScreen( inputValid = model.inputValid, @@ -61,8 +69,8 @@ fun EditSubscriptionScreen( onDelete = model::removeSubscription, onSave = model::updateSubscription, onShare = { - subscription.value?.let { - onShare(it) + subscriptionWithCredential?.let { (subscription) -> + onShare(subscription) } }, onExit = onExit, From 479c5e505b4e76ce1b8a1af39a0be20f7526b64b Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Wed, 27 Aug 2025 15:34:50 +0200 Subject: [PATCH 5/8] Moved viewmodel loading start to viewmodel init Signed-off-by: Arnau Mora --- .../icsdroid/db/dao/SubscriptionsDao.kt | 3 +++ .../icsdroid/model/EditSubscriptionModel.kt | 19 ++++++++++++++----- .../ui/screen/EditSubscriptionScreen.kt | 11 +---------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/db/dao/SubscriptionsDao.kt b/app/src/main/java/at/bitfire/icsdroid/db/dao/SubscriptionsDao.kt index 0089f5c5..29e0912f 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/dao/SubscriptionsDao.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/dao/SubscriptionsDao.kt @@ -49,6 +49,9 @@ interface SubscriptionsDao { @Query("SELECT * FROM subscriptions WHERE id=:id") fun getWithCredentialsByIdFlow(id: Long): Flow + @Query("SELECT * FROM subscriptions WHERE id=:id") + suspend fun getWithCredentialsById(id: Long): SubscriptionWithCredential? + @Query("SELECT errorMessage FROM subscriptions WHERE id=:id") fun getErrorMessageFlow(id: Long): Flow diff --git a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt index b66fdb5a..2f8c845e 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt @@ -24,7 +24,6 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @HiltViewModel(assistedFactory = EditSubscriptionModel.EditSubscriptionModelFactory::class) @@ -82,18 +81,28 @@ class EditSubscriptionModel @AssistedInject constructor( var successMessage: String? by mutableStateOf(null) private set - val subscriptionWithCredential = db.subscriptionsDao().getWithCredentialsByIdFlow(subscriptionId) + var subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential? = null + private set + + init { + viewModelScope.launch { + db.subscriptionsDao().getWithCredentialsById(subscriptionId)?.let { + onSubscriptionLoaded(it) + } + } + } /** * Initialise view models and remember their initial state */ - fun onSubscriptionLoaded(subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential) { + private fun onSubscriptionLoaded(subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential) { val subscription = subscriptionWithCredential.subscription val credential = subscriptionWithCredential.credential // Save the initial state, before updating the UI, so the state is persisted initialSubscription = subscription initialCredential = credential + this.subscriptionWithCredential = subscriptionWithCredential subscriptionSettingsUseCase.update(subscription, credential) } @@ -103,7 +112,7 @@ class EditSubscriptionModel @AssistedInject constructor( */ fun updateSubscription() = with(subscriptionSettingsUseCase.uiState) { viewModelScope.launch(Dispatchers.IO) { - subscriptionWithCredential.firstOrNull()?.let { (subscription) -> + subscriptionWithCredential?.let { (subscription) -> val newSubscription = subscription.copy( displayName = title ?: subscription.displayName, color = color, @@ -134,7 +143,7 @@ class EditSubscriptionModel @AssistedInject constructor( */ fun removeSubscription() { viewModelScope.launch(Dispatchers.IO) { - subscriptionWithCredential.firstOrNull()?.let { (subscription) -> + subscriptionWithCredential?.let { (subscription) -> db.subscriptionsDao().delete(subscription) // sync the subscription to reflect the changes in the calendar provider diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditSubscriptionScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditSubscriptionScreen.kt index 3b4b440b..5f4b50a2 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditSubscriptionScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditSubscriptionScreen.kt @@ -22,9 +22,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -35,7 +32,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import at.bitfire.icsdroid.R import at.bitfire.icsdroid.db.entity.Subscription import at.bitfire.icsdroid.model.EditSubscriptionModel @@ -55,11 +51,6 @@ fun EditSubscriptionScreen( val model = hiltViewModel { factory -> factory.create(subscriptionId) } - val subscriptionWithCredential by model.subscriptionWithCredential.collectAsState(null) - - LaunchedEffect(subscriptionWithCredential) { - subscriptionWithCredential?.let { model.onSubscriptionLoaded(it) } - } with(model.subscriptionSettingsUseCase) { EditSubscriptionScreen( @@ -69,7 +60,7 @@ fun EditSubscriptionScreen( onDelete = model::removeSubscription, onSave = model::updateSubscription, onShare = { - subscriptionWithCredential?.let { (subscription) -> + model.subscriptionWithCredential?.let { (subscription) -> onShare(subscription) } }, From f5f14defbfb498256e841af42ee69acf89a86bc4 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Wed, 27 Aug 2025 15:35:20 +0200 Subject: [PATCH 6/8] Removed unused function Signed-off-by: Arnau Mora --- .../main/java/at/bitfire/icsdroid/db/dao/SubscriptionsDao.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/db/dao/SubscriptionsDao.kt b/app/src/main/java/at/bitfire/icsdroid/db/dao/SubscriptionsDao.kt index 29e0912f..3cbb7aa0 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/dao/SubscriptionsDao.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/dao/SubscriptionsDao.kt @@ -46,9 +46,6 @@ interface SubscriptionsDao { @Query("SELECT * FROM subscriptions WHERE url=:url") suspend fun getByUrl(url: String): Subscription? - @Query("SELECT * FROM subscriptions WHERE id=:id") - fun getWithCredentialsByIdFlow(id: Long): Flow - @Query("SELECT * FROM subscriptions WHERE id=:id") suspend fun getWithCredentialsById(id: Long): SubscriptionWithCredential? From 479df2e4ea3890b116d381b9b1c689eebaaa4c98 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Wed, 27 Aug 2025 15:36:24 +0200 Subject: [PATCH 7/8] `initialRequiresAuth` is never null Signed-off-by: Arnau Mora --- .../java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt index 2f8c845e..547d90b7 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/EditSubscriptionModel.kt @@ -41,7 +41,7 @@ class EditSubscriptionModel @AssistedInject constructor( private var initialSubscription: Subscription? = null private var initialCredential: Credential? = null - private val initialRequiresAuth: Boolean? get() = initialCredential != null + private val initialRequiresAuth: Boolean get() = initialCredential != null /** * Whether user input is error free From 44316cd5fecc7da0384894e7e62c617eb437cc7e Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Wed, 27 Aug 2025 15:39:36 +0200 Subject: [PATCH 8/8] Add missing `customUserAgent` to `update` Signed-off-by: Arnau Mora --- .../at/bitfire/icsdroid/model/SubscriptionSettingsUseCase.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/at/bitfire/icsdroid/model/SubscriptionSettingsUseCase.kt b/app/src/main/java/at/bitfire/icsdroid/model/SubscriptionSettingsUseCase.kt index 8f1cf6f2..233f7f1c 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/SubscriptionSettingsUseCase.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/SubscriptionSettingsUseCase.kt @@ -86,6 +86,7 @@ class SubscriptionSettingsUseCase @Inject constructor() { url = subscription.url.toString(), title = subscription.displayName, color = subscription.color, + customUserAgent = subscription.customUserAgent, ignoreAlerts = subscription.ignoreEmbeddedAlerts, defaultAlarmMinutes = subscription.defaultAlarmMinutes, defaultAllDayAlarmMinutes = subscription.defaultAllDayAlarmMinutes,