Loading app/src/androidTestOse/kotlin/at/bitfire/davdroid/syncadapter/AccountUtilsTest.kt +24 −1 Original line number Diff line number Diff line Loading @@ -6,10 +6,14 @@ package at.bitfire.davdroid.syncadapter import android.accounts.Account import android.accounts.AccountManager import android.net.Uri import android.os.Bundle import androidx.test.platform.app.InstrumentationRegistry import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager import com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Assert.assertEquals Loading Loading @@ -37,6 +41,7 @@ class AccountUtilsTest { val context by lazy { InstrumentationRegistry.getInstrumentation().targetContext } val account by lazy { Account("Test Account", context.getString(R.string.account_type)) } val murenaAccount by lazy { Account("Murena Test Account", context.getString(R.string.eelo_account_type)) } @Test fun testCreateAccount() { Loading @@ -56,4 +61,22 @@ class AccountUtilsTest { } } @Test fun isMurenaAccount_persistsWorkspaceDomainForLegacyAccounts() { val userData = Bundle(1) userData.putString(KEY_OC_BASE_URL, BuildConfig.MURENA_BASE_URL_PRODUCTION) try { assertTrue(AccountUtils.createAccount(context, murenaAccount, userData)) assertTrue(AccountUtils.isMurenaAccount(context, murenaAccount)) val expectedDomain = Uri.parse(AccountUtils.extractBaseUrl(BuildConfig.MURENA_BASE_URL_PRODUCTION)).host val storedDomain = AccountManager.get(context).getUserData(murenaAccount, AccountSettings.KEY_WORKSPACE_DOMAIN) assertEquals(expectedDomain, storedDomain) } finally { val futureResult = AccountManager.get(context).removeAccount(murenaAccount, {}, null) assertTrue(futureResult.getResult(10, TimeUnit.SECONDS)) } } } app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +1 −1 Original line number Diff line number Diff line Loading @@ -60,7 +60,7 @@ class AccountSettings( companion object { const val CURRENT_VERSION = 15 const val CURRENT_VERSION = 16 const val KEY_SETTINGS_VERSION = "version" const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks" Loading app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt +40 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ import android.util.Base64 import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import at.bitfire.davdroid.Constants import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection Loading @@ -23,6 +24,7 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.LocalTask import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.util.setAndVerifyUserData Loading @@ -33,6 +35,7 @@ import at.bitfire.ical4android.UnknownProperty import at.bitfire.vcard4android.ContactsStorageException import at.bitfire.vcard4android.GroupMethod import at.techbee.jtx.JtxContract.asSyncAdapter import com.owncloud.android.lib.common.accounts.AccountUtils.Constants as NCAccountUtilsConstants import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.property.Url import okhttp3.HttpUrl.Companion.toHttpUrlOrNull Loading @@ -50,6 +53,42 @@ class AccountSettingsMigrations( val accountSettings: AccountSettings ) { /** * Persist explicit workspace backend identity for legacy Murena accounts that predate * descriptor persistence. */ fun update_15_16() { if (account.type != context.getString(R.string.eelo_account_type)) { return } if (!accountManager.getUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN).isNullOrBlank()) { return } val baseUrl = accountManager .getUserData(account, NCAccountUtilsConstants.KEY_OC_BASE_URL) ?.let(AccountUtils::extractBaseUrl) ?: return val murenaBaseUrls = setOf( AccountUtils.extractBaseUrl(BuildConfig.MURENA_BASE_URL_PRODUCTION), AccountUtils.extractBaseUrl(BuildConfig.MURENA_BASE_URL_STAGING) ) if (baseUrl !in murenaBaseUrls) { return } val workspaceDomain = baseUrl.toHttpUrlOrNull()?.host if (workspaceDomain.isNullOrBlank()) { Logger.log.warning("Could not derive workspace domain from legacy base URL: $baseUrl") return } accountManager.setAndVerifyUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN, workspaceDomain) } /** * We may get upstream conflict for later update because of version number. * from Murena's Squad Loading app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +11 −2 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ package at.bitfire.davdroid.syncadapter import android.accounts.Account import android.accounts.AccountManager import android.content.Context import android.net.Uri import android.os.Bundle import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R Loading Loading @@ -175,7 +176,7 @@ object AccountUtils { if (workspaceDomain != null) return true // Legacy path: derive identity from URL and account type for accounts that predate // workspace descriptor persistence. // workspace descriptor persistence, then persist explicit backend identity. val urlData = accountManager.getUserData(account, Constants.KEY_OC_BASE_URL) ?: return false Loading @@ -190,7 +191,15 @@ object AccountUtils { val isMurenaCloud = baseUrl in murenaBaseUrls val isMurenaAccountType = (account.type == context.getString(R.string.eelo_account_type)) return isMurenaCloud && isMurenaAccountType if (!isMurenaCloud || !isMurenaAccountType) { return false } Uri.parse(baseUrl).host?.let { domain -> accountManager.setAndVerifyUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN, domain) } return true } fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { Loading doc/workspace-backend-compatibility.md 0 → 100644 +47 −0 Original line number Diff line number Diff line # Workspace Backend Compatibility (Legacy Murena Accounts) Related work items: - https://gitlab.e.foundation/e/os/AccountManager/-/work_items/114 - https://gitlab.e.foundation/e/os/AccountManager/-/work_items/118 ## Goal Keep already provisioned public Murena accounts working while moving to explicit backend identity and backend-portable behavior. ## Account Shapes and Compatibility Path 1. New Murena Workspace account - Shape: `workspace_domain` is present in Android account userData. - Behavior: account is identified from explicit backend metadata. - Compatibility note: no legacy fallback needed. 2. Legacy public Murena account (pre-descriptor persistence) - Shape: `workspace_domain` is missing, but account type is `eelo_account_type` and `oc_base_url` matches known Murena public base URLs. - Behavior: legacy detection remains available, then `workspace_domain` is persisted. - Compatibility note: this is a lazy resolution path to converge to explicit metadata. 3. Migrated legacy Murena account - Shape: account was created before descriptor persistence, then migration `15 -> 16` ran. - Behavior: `workspace_domain` is backfilled from legacy base URL and persisted durably. - Compatibility note: subsequent behavior no longer depends on implicit Murena defaults. 4. Non-Murena or custom backend account - Shape: no `workspace_domain`, and Murena legacy URL/type check does not match. - Behavior: not treated as Murena Workspace account. - Compatibility note: no Murena-specific backfill is applied. ## Strategy Summary - Migration path: account settings migration `update_15_16` backfills explicit backend identity for compatible legacy public Murena accounts. - Lazy path: legacy detection in `isMurenaAccount()` persists `workspace_domain` when it can infer Murena identity safely. - Safety: only known public Murena base URLs are used for legacy backfill. ## Expected Outcome - Existing public Murena accounts continue to function. - Backend identity becomes explicit and durable over time. - Runtime behavior progressively stops depending on implicit defaults. Loading
app/src/androidTestOse/kotlin/at/bitfire/davdroid/syncadapter/AccountUtilsTest.kt +24 −1 Original line number Diff line number Diff line Loading @@ -6,10 +6,14 @@ package at.bitfire.davdroid.syncadapter import android.accounts.Account import android.accounts.AccountManager import android.net.Uri import android.os.Bundle import androidx.test.platform.app.InstrumentationRegistry import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager import com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Assert.assertEquals Loading Loading @@ -37,6 +41,7 @@ class AccountUtilsTest { val context by lazy { InstrumentationRegistry.getInstrumentation().targetContext } val account by lazy { Account("Test Account", context.getString(R.string.account_type)) } val murenaAccount by lazy { Account("Murena Test Account", context.getString(R.string.eelo_account_type)) } @Test fun testCreateAccount() { Loading @@ -56,4 +61,22 @@ class AccountUtilsTest { } } @Test fun isMurenaAccount_persistsWorkspaceDomainForLegacyAccounts() { val userData = Bundle(1) userData.putString(KEY_OC_BASE_URL, BuildConfig.MURENA_BASE_URL_PRODUCTION) try { assertTrue(AccountUtils.createAccount(context, murenaAccount, userData)) assertTrue(AccountUtils.isMurenaAccount(context, murenaAccount)) val expectedDomain = Uri.parse(AccountUtils.extractBaseUrl(BuildConfig.MURENA_BASE_URL_PRODUCTION)).host val storedDomain = AccountManager.get(context).getUserData(murenaAccount, AccountSettings.KEY_WORKSPACE_DOMAIN) assertEquals(expectedDomain, storedDomain) } finally { val futureResult = AccountManager.get(context).removeAccount(murenaAccount, {}, null) assertTrue(futureResult.getResult(10, TimeUnit.SECONDS)) } } }
app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +1 −1 Original line number Diff line number Diff line Loading @@ -60,7 +60,7 @@ class AccountSettings( companion object { const val CURRENT_VERSION = 15 const val CURRENT_VERSION = 16 const val KEY_SETTINGS_VERSION = "version" const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks" Loading
app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt +40 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ import android.util.Base64 import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import at.bitfire.davdroid.Constants import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection Loading @@ -23,6 +24,7 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.LocalTask import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.util.setAndVerifyUserData Loading @@ -33,6 +35,7 @@ import at.bitfire.ical4android.UnknownProperty import at.bitfire.vcard4android.ContactsStorageException import at.bitfire.vcard4android.GroupMethod import at.techbee.jtx.JtxContract.asSyncAdapter import com.owncloud.android.lib.common.accounts.AccountUtils.Constants as NCAccountUtilsConstants import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.property.Url import okhttp3.HttpUrl.Companion.toHttpUrlOrNull Loading @@ -50,6 +53,42 @@ class AccountSettingsMigrations( val accountSettings: AccountSettings ) { /** * Persist explicit workspace backend identity for legacy Murena accounts that predate * descriptor persistence. */ fun update_15_16() { if (account.type != context.getString(R.string.eelo_account_type)) { return } if (!accountManager.getUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN).isNullOrBlank()) { return } val baseUrl = accountManager .getUserData(account, NCAccountUtilsConstants.KEY_OC_BASE_URL) ?.let(AccountUtils::extractBaseUrl) ?: return val murenaBaseUrls = setOf( AccountUtils.extractBaseUrl(BuildConfig.MURENA_BASE_URL_PRODUCTION), AccountUtils.extractBaseUrl(BuildConfig.MURENA_BASE_URL_STAGING) ) if (baseUrl !in murenaBaseUrls) { return } val workspaceDomain = baseUrl.toHttpUrlOrNull()?.host if (workspaceDomain.isNullOrBlank()) { Logger.log.warning("Could not derive workspace domain from legacy base URL: $baseUrl") return } accountManager.setAndVerifyUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN, workspaceDomain) } /** * We may get upstream conflict for later update because of version number. * from Murena's Squad Loading
app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +11 −2 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ package at.bitfire.davdroid.syncadapter import android.accounts.Account import android.accounts.AccountManager import android.content.Context import android.net.Uri import android.os.Bundle import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R Loading Loading @@ -175,7 +176,7 @@ object AccountUtils { if (workspaceDomain != null) return true // Legacy path: derive identity from URL and account type for accounts that predate // workspace descriptor persistence. // workspace descriptor persistence, then persist explicit backend identity. val urlData = accountManager.getUserData(account, Constants.KEY_OC_BASE_URL) ?: return false Loading @@ -190,7 +191,15 @@ object AccountUtils { val isMurenaCloud = baseUrl in murenaBaseUrls val isMurenaAccountType = (account.type == context.getString(R.string.eelo_account_type)) return isMurenaCloud && isMurenaAccountType if (!isMurenaCloud || !isMurenaAccountType) { return false } Uri.parse(baseUrl).host?.let { domain -> accountManager.setAndVerifyUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN, domain) } return true } fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { Loading
doc/workspace-backend-compatibility.md 0 → 100644 +47 −0 Original line number Diff line number Diff line # Workspace Backend Compatibility (Legacy Murena Accounts) Related work items: - https://gitlab.e.foundation/e/os/AccountManager/-/work_items/114 - https://gitlab.e.foundation/e/os/AccountManager/-/work_items/118 ## Goal Keep already provisioned public Murena accounts working while moving to explicit backend identity and backend-portable behavior. ## Account Shapes and Compatibility Path 1. New Murena Workspace account - Shape: `workspace_domain` is present in Android account userData. - Behavior: account is identified from explicit backend metadata. - Compatibility note: no legacy fallback needed. 2. Legacy public Murena account (pre-descriptor persistence) - Shape: `workspace_domain` is missing, but account type is `eelo_account_type` and `oc_base_url` matches known Murena public base URLs. - Behavior: legacy detection remains available, then `workspace_domain` is persisted. - Compatibility note: this is a lazy resolution path to converge to explicit metadata. 3. Migrated legacy Murena account - Shape: account was created before descriptor persistence, then migration `15 -> 16` ran. - Behavior: `workspace_domain` is backfilled from legacy base URL and persisted durably. - Compatibility note: subsequent behavior no longer depends on implicit Murena defaults. 4. Non-Murena or custom backend account - Shape: no `workspace_domain`, and Murena legacy URL/type check does not match. - Behavior: not treated as Murena Workspace account. - Compatibility note: no Murena-specific backfill is applied. ## Strategy Summary - Migration path: account settings migration `update_15_16` backfills explicit backend identity for compatible legacy public Murena accounts. - Lazy path: legacy detection in `isMurenaAccount()` persists `workspace_domain` when it can infer Murena identity safely. - Safety: only known public Murena base URLs are used for legacy backfill. ## Expected Outcome - Existing public Murena accounts continue to function. - Backend identity becomes explicit and durable over time. - Runtime behavior progressively stops depending on implicit defaults.