Allow CRUD operations with database accounts - #2823
Conversation
98f256a to
0af97ac
Compare
0af97ac to
76f9b26
Compare
216b626 to
36a531f
Compare
createDbAccountThere was a problem hiding this comment.
Pull request overview
Adds support for “database accounts” (DbAccountId) across account creation, listing, sync tagging, and service lookups so DAVx⁵ can manage accounts stored in the Room database while still maintaining an Android system-account representation.
Changes:
- Switch account creation to a suspending
AccountRepository.create()that creates aDbAccount+ initial settings and then creates the corresponding Android account. - Extend account enumeration, renaming, and deletion paths to handle both
LegacyAccountandDbAccountId. - Update sync tagging and service DAO accessors to support
DbAccountId-based operations.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| core/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreenViewModel.kt | Uses the new suspending account creation API during login/setup. |
| core/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt | Adds DbAccountId support to WorkManager tag generation. |
| core/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt | Adds helper to persist initial settings into the account settings store. |
| core/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt | Implements DB-backed account creation and extends delete/rename/list/flow APIs for DbAccountId. |
| core/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt | Enables service lookups by DbAccountId by resolving account name from DB. |
| core/src/main/kotlin/at/bitfire/davdroid/db/AccountSettingDao.kt | Removes an unused blocking query for all settings of an account. |
| core/src/main/kotlin/at/bitfire/davdroid/accounts/AndroidAccountManager.kt | Resolves an Android Account for DbAccountId via DB lookup. |
Suppressed comments (2)
core/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt:173
- issue (blocking): Database account rows are left behind when Android account creation fails.
DbAccount (and related AccountSettings via the FK cascade) is inserted before AndroidAccountUtils.createAccount(). If createAccount() returns false (for instance because the system account name already exists), this currently returns null without removing the DB row, which can hide the existing system account from listings (because system accounts are filtered by DB names).
This issue also appears on line 201 of the same file.
logger.log(Level.INFO, "Creating Android account {0} with initial config {1}", arrayOf(account, userData))
if (!AndroidAccountUtils.createAccount(context, account, userData, credentials?.password))
return null
core/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt:204
- issue (blocking): Failed account creation leaves a partially created DB account.
If an InvalidAccountException is thrown while writing the discovered services/settings, the method returns null but keeps the inserted DbAccount + settings in the database. This can break later lookups and also hide the system account in getAll*() due to name filtering.
} catch (e: InvalidAccountException) {
logger.log(Level.SEVERE, "Couldn't access account settings", e)
return null
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@rfc2822 I've added you too as a reviewer since this is such an important piece of code. So even though it's not a huge change, its impact is quite high, and design choices have been made, so I believe you can also take a look. |
rfc2822
left a comment
There was a problem hiding this comment.
Added some things that I noticed.
(Some may need further discussion to clarify what approach we want.)
| private val runningSyncs = Collections.synchronizedSet(HashSet<String>()) | ||
|
|
||
| /** | ||
| * This tag shall be added to every worker that is enqueued by a subclass. |
There was a problem hiding this comment.
Todo: We should add why this tag should be added – to identify the worker for whoever wants to know whether this sync is already enqueued/running.
| "sync-$dataType ${account.type}/${account.name}" | ||
| } | ||
| is DbAccountId -> TODO("Operation not yet implemented") | ||
| is DbAccountId -> "sync-$dataType ${accountId.id}" |
There was a problem hiding this comment.
Thought: I identified these current uses:
- sync worker: prevent multiple concurrent syncs with same tag
- UI: determine whether a specific sync is enqueued/in progress
- debug info: dump workers
So these would be affected if an account has a new tag (for instance after the AccountManager → DB migration).
I think none of these will break when the tag is not 1:1 the same after such a migration, but we should keep it in mind.
| // note: this currently depends on the account being created before. at some point, the system accounts | ||
| // should be dynamically created, and this method should be able to create the account if it | ||
| // doesn't exist yet. for now, we just throw an exception. | ||
| // note: currently, this is using runBlocking. ideally we should make the function suspending | ||
| runBlocking(ioDispatcher) { dbAccountDao.get(accountId.id) } | ||
| ?.let { Account(it.name, accountType) } | ||
| ?: throw NoSuchElementException("No account found for id $accountId") |
There was a problem hiding this comment.
Issue: I think this method should always return a qualified, existing Android Account (this is what KDoc says).
I'd let this method unimplemented for now and do it in #843.
| is DbAccountId -> { | ||
| val account = dbAccountDao.get(accountId.id) ?: return null | ||
| getByAccountAndType(account.name, type) | ||
| } |
There was a problem hiding this comment.
Issue: Right now (Android account) we have to use account.name as reference because we don't have a a better one. As a consequence, we need to update the service when we for instance rename an account.
However the new AccountId brings us the chance to properly reference accounts by ID. So I think the service table should get a new nullable accountId column that references the DB account, instead of continuing to use the name.
As soon as we have all accounts in the DB (after all migrations are enforced), we can make the accountId non-nullable and then always have a working foreign key.
| * @see initialUserData | ||
| */ | ||
| fun putInitialSettings(credentials: Credentials?, preconfigurationUrl: String?) { | ||
| val all = initialUserData(credentials, preconfigurationUrl) |
There was a problem hiding this comment.
Suggestion: The initial user data is now a Map<String,String>, which is not the correct choice, because we want to map the keys to either String or SensitiveString. Thus the confusion that makes the putInitialSettings harder to understand.
I suggest to refactor initialUserData in a separate PR (before this PR) so that it returns a result object, probably of a type like
initialUserData = Map<String,InitialUserSettingValue>
interface InitialUserSettingValue {
value class StringSetting(value: String)
class SensitiveStringSetting(value: SensitiveString)
}or, when we like to keep it simpler, just a
data class InitialUserSettings(
val values: Map<String,String>,
val sensitiveValues: Map<String,SensitiveString>
)Then we can treat the password like any other value and don't have to hardcode KEY_PASSWORD in putInitialSettings.
0a7035a to
21e134f
Compare
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Lost when rebasing onto the -service branch: ServiceDao became an abstract class in this branch's own history, but the accountId-based query methods (added on the -service branch, written when it was still an interface) weren't updated to match.
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
76dcae5 to
b1178c9
Compare
Allow deleting accounts from database in `delete` Allow getting account name in `getAccountName` and `getAccountNameBlocking` Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Lost when rebasing onto the -service branch: ServiceDao became an abstract class in this branch's own history, but the accountId-based query methods (added on the -service branch, written when it was still an interface) weren't updated to match.
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
b1178c9 to
5797f67
Compare
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
…o-the-database-repository' into 2820-allow-creating-accounts-into-the-database-repository # Conflicts: # core/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Organizational
Important
Please make sure an issue or discussion for this change exists and was acknowledged by the
core team before submitting. Uncoordinated pull requests may conflict with the project roadmap
and could be closed without merging — which would be a pity.
See CONTRIBUTING.md for details.
(The PR is still welcome otherwise, but could be closed at any time without much attention
when it doesn't fit.)
✨ AI contributions – only check if applicable:
is just referenced/copied into an AI prompt and then the result is submitted as PR.
PRs that are mainly driven by AI often have little worth, as everybody could just copy the issue
into a prompt. The worth of a good PR often comes from the main idea, architectural decisions etc.
behind it. So while all PRs are welcome, such PRs may be reviewed with less human attention and closed
at any time when they don't fit.
Purpose
Allow creating accounts into the database
Short description
createtoAccountRepository, which is suspending, and creates the account into the database, as well as the system.deleteso that it also allows deleting db accounts.renameso that it also allows renaming db accounts.By default, now the accounts are created into the database. When doing so, a copy of the account is created into the system (following the same pattern as we had).
When listing accounts, they are listed both from system, and from database. Since database accounts are also in the system, they are filtered out from the system accounts list.
Checklist
Stack created with GitHub Stacks CLI • Give Feedback 💬