Add KeeShare per-device sync support#2438
Conversation
KeeShare reference parsing (classic base64 XML), per-device sync config (SAF URI-based), container read/write via streams, import/ export orchestration with scoped merge, device identity generation, and group-level merge support in DatabaseKDBXMerger. All I/O is stream-based — callers provide InputStream/OutputStream lambdas. No direct filesystem access in this layer.
KeeShareSyncRunnable bridges core layer to SAF via DocumentFile and ContentResolver streams. KeeShareSyncManager handles periodic sync, export-on-save, and stale container cleanup. UI: "Sync KeeShare" menu item in GroupActivity, result toast in DatabaseLockActivity, share indicator on groups, settings screen with device ID preference. All sync directory access uses SAF tree URIs — no filesystem paths or inotify watchers.
Group and entry custom data items had their lastModificationTime values read but discarded during XML parsing. This caused merge conflict resolution in mergeCustomData() to always prefer incoming data regardless of timestamps.
Implementation design doc, technical reference (keeshare-support.md), and end-user setup guide covering both desktop and Android sides.
KeePassXC uses ImportFrom=1, ExportTo=2 as bitmask flags, not enum ordinals. Also adds keepGroups support for per-device config.
SAF-based folder picker with persistent URI permissions. Shows selected folder in preference summary.
When groups have a classic KeeShare/Reference from KeePassXC but no PerDeviceSync config yet, prompts user to pick the local sync folder before running sync.
Adds per-group config logging during import scan, tracks actual new entries added vs container entry count.
Auto-triggered syncs (ContentObserver/polling) now only import — no export. This matches KeePassXC's pattern where directory changes trigger import-only, and database saves trigger export-only. Also bumps ContentObserver debounce to 3s and adds 5s min-interval guard to prevent rapid-fire re-triggers from Syncthing writes.
Documents the sync folder picker, first-time configuration flow, import-only auto-sync vs export-on-save split, and new troubleshooting entries.
Makes it explicit that KeePassXC must be configured first (writes CustomData), then the database is copied to the phone, then KeePassDX is configured. Adds root group sharing warning.
Track lastModified() per imported .kdbx file in SharedPreferences. Before opening a stream (and triggering expensive Argon2 decryption), compare the file's current mtime against the stored value and skip if unchanged. Mtimes are persisted after each successful import.
When startAutoSync() runs, launch a one-shot check for newer container files before waiting for the periodic poll. Catches files delivered by Syncthing while the app was closed or locked.
|
Two additional commits pushed:
Tracks
When |
| @@ -0,0 +1,91 @@ | |||
| name: Release Build | |||
There was a problem hiding this comment.
This file is part of the GitHub CD; it has no place in the PR.
Another issue is needed for this, the problem will be signing the APK with the keys currently used for production, although this may be useful for debug versions.
| // all changes, e.g. files synced by external tools writing to filesystem) | ||
| periodicJob = CoroutineScope(Dispatchers.IO).launch { | ||
| while (isActive) { | ||
| delay(PERIODIC_SYNC_INTERVAL_MS) |
There was a problem hiding this comment.
My previous comment still stands: there should be no polling.
| override fun onActionRun() { | ||
| Log.d(TAG, "=== KeeShare sync onActionRun() START ===") | ||
| try { | ||
| val kdbx = database.databaseKDBX |
There was a problem hiding this comment.
The level of abstraction is not good. Historically, there may be actions that are performed directly on a specific type of database. But it needs to be generalized so that only the database type is used.
|
|
||
| val deviceId = resolveDeviceId(context) | ||
| Log.d(TAG, "Device ID: $deviceId") | ||
| val cacheDir = File(context.cacheDir, "keeshare") |
There was a problem hiding this comment.
There should be no direct cache in KeePassDX.
| val data = result.data | ||
| if (data != null) { | ||
| val imported = data.getInt( | ||
| com.kunzisoft.keepass.database.action.KeeShareSyncRunnable.RESULT_IMPORTED_ENTRIES, 0) |
There was a problem hiding this comment.
There is no need to include the full package names for each constant. This is the case everywhere.
| * but no KeeShare/PerDeviceSync custom data yet. The user needs to pick | ||
| * a local SAF directory for these. | ||
| */ | ||
| private var mPendingKeeShareGroups: List<com.kunzisoft.keepass.database.element.group.GroupKDBX> = emptyList() |
There was a problem hiding this comment.
The new code must be placed in the viewModel; keeping this type of variable in the activities will make migration more difficult.
| // Persist read/write access across reboots | ||
| val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or | ||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION | ||
| contentResolver.takePersistableUriPermission(treeUri, takeFlags) |
There was a problem hiding this comment.
Permissions for opening files and folders should not be defined in the activity; instead, the common SAF code for opening files and folders should be aggregated.
|
|
||
| /** Public read-only access to the KDBX database, or null if this is a KDB database */ | ||
| val databaseKDBX: DatabaseKDBX? | ||
| get() = mDatabaseKDBX |
There was a problem hiding this comment.
No, there must be an abstraction so that there is no direct access to the database type. Even though the existing code may be written this way in several places, it is a legacy architecture from KeePassDroid and will be reviewed during migration.
|
|
||
| **Branch**: `feature/keeshare-saf-refactor` | ||
| **Base**: `feature/keeshare-support` | ||
| **Date**: 2026-02-23 |
|
|
||
| - **ContentObserver**: Near-real-time detection of new files (fires within | ||
| seconds of your sync tool writing a new container) | ||
| - **Periodic poll**: Fallback check every 2 minutes in case the |
There was a problem hiding this comment.
ContentObserver should be sufficient. If there is a failure, the application does not have to compensate for the file provider's problems.
|
There is a problem with the basic design of this feature. Initially, KeePassDX was designed not to manage the concept of files at all and to abstract itself from the data type in order to process only data streams that can be saved anywhere (even in blobs scattered only in RAM if we want to implement it through SAF). But here, the concept of predefined files has been reimplemented by matching devices and even processing file names for a sub-feature, even though the Root database is not capable of doing so. The features must therefore remain consistent.
This feature as it is currently written adds many redundant concepts. In my opinion, creating a hybrid granularity system is a very bad idea, both for the creation of new features, which will be much more difficult to create because we will have to consider dual implementation, and for maintenance. I haven't finished the review, but I've already spent a lot of time on it. ;) |
|
KeeShare per-device mode exists in KeePassXC because file sync tools don't understand KDBX merge semantics. Syncthing can avoid file write conflicts, but it can't merge password entries from two databases. That's why KeeShare splits into per-device files and then does KDBX-level merging. Your alternative, "a generic FileSync app", would need to understand KDBX internals anyway to do meaningful merges, which defeats the "generic" purpose.
|
|
I've got a working solution, and my fork I've pushed working binaries. I'm using it already, so.... it is what it is. Do what thou wilt. |
Yes, I understand. The idea is to offer the same URI that corresponds to a file but that will be merged when its content changes.
No, that's the whole point of completely separating the concepts.
I didn't know what “vaporware” meant, so I looked up the definition. I'm working on this application locally, and creating a complete file manager is no easy task, especially if you want to integrate a versioning system. I decided to prioritize KeePassDX, which I think is the right choice. If FileSync isn't available yet, it's because there's no point in launching an application that's only half finished and has bugs. Vaporware? I realize it's easy to criticize, but I work on this in my spare time and I'm not under contract, so there are no deadlines and the project isn't canceled, apparently that's not the right term; so it's just there to disparage. I simply want to produce high-quality projects to try to compensate as much as possible for the mistakes of other apps that do not consider quality to be a requirement. Therefore, comments of this kind are not at all welcome.
Why terrible UX? It hasn't been done yet. Doing a little bit of both halfway isn't better, even if it meets your immediate needs, because it means there will be structural problems later on. You could say that since this is a future maintenance issue, we could just ignore it as long as it works now, but you know, that's not my style. ;)
If ContentObserver is known to be unreliable, why not propose corrections to the underlying problem? It's not ContentObserver that's not viable, it's the file providers who don't honor SAF contracts. These are two different things.
I disagree. With another example, people said the same thing about Passkey, and it was still done entirely locally, it was unrealistic but theoretically feasible, and it was done. If there is a clear, long-term vision and the necessary resources are put in place, there is no reason why it shouldn't work. It may be unrealistic for one generation of fast code, but not over time.
I want to hear real arguments here, because it's specifically for the users that I want the architecture to remain clean and with a long-term vision. Thinking about the overall coherence of an ecosystem and solving structural problems are the very basis of tools designed for security. If practical utility means making compromises that would undermine the long-term stability of the application, that's not the basic philosophy here. Strong basis with is a conscious choice with real encapsulations has been part of the application since its inception and is the reason why it was created. So without arguments and without having understood that at first, again, it's just an AInsult.
I appreciate your involvement, because it is very instructive, particularly with regard to the conditioned behavior of AI in relation to the context provided. I understand that most people want a basic tool that works but I don't understand why you give up so easily. As an anecdote, if I had stuck with that idea to have only one tool that works, KeePassDX wouldn't exist and I will continue to use KeePass2Android. The idea here is to have a healthy debate to create quality tools. If your fork meets your needs and those of other users, that's really great. I'm simply saying that features need to be thought out and designed holistically so that there are no structural problems in the long term. |
- remove periodic polling from KeeShareSyncManager, keep ContentObserver - make databaseKDBX internal to database module - remove explicit cacheDirectory param from import/export - add Database.forEachGroupWithCustomData helper - replace FQN references with proper imports throughout - use abstract Group type instead of GroupKDBX in activities - use UriUtil.takeUriPermission for SAF permissions - update end-user guide to remove polling references
|
Pushed fixes addressing review feedback: Done:
Not done:
|
|
Ah, you know, the French can be credited with originating the word for sabotage — the sabot, the little wooden shoe flung into the gears, the romance of defiance, the worker’s shoe interrupting the march of industry. It is such a theatrical image, so satisfyingly dramatic. One can almost hear the clatter. But the truth is less cinematic and far more amusing. If the French contributed the poetry of obstruction, the Americans refined its technique. During the war, the OSS published a charmingly practical handbook titled Simple Sabotage. And what did it recommend? Not fire. Not force. Not shoes. Meetings. Insist on proper channels. Refer everything to committees. Request further study. Raise small procedural objections. Reopen settled decisions for “clarification.” Debate wording. Demand exhaustive documentation. Advocate caution in the name of responsibility. Pursue perfection with such devotion that nothing may proceed until it is immaculate. Obstructionist Perfectionism. You see, smashing the machine is crude. Anyone can break a gear. True obstruction is more elegant — it dresses itself as diligence, as prudence, as professionalism. It smiles warmly and says, “Let us be thorough.” And so progress does not collapse in flames. It gently suffocates under minutes, subcommittees, and the noble pursuit of doing everything exactly right. It is almost beautiful, in its way. So tell me my dear Jamet why is a five year old project stuck on something I did in one day? |
|
I don't see why I would sabotage a pull request for a feature that I also want and that would satisfy users? It makes no sense. I was clear about my arguments, so in your opinion, if I were really a "saboteur", why would I waste my time doing this review and responding to your requests? I never said that the pull request would not be integrated. There are good things, and it's an interesting approach. I am just being cautious, especially regarding the architecture, security and maintenance, because this is a big PR and the first of this type that has been made, it duplicates concepts, and I don't understand why you are starting to get aggressive even though it is completely legitimate. You may be expecting praise because it works, and since my review is slower than your automatic code generation, you don't like it, but what you're saying about nationality is completely irrelevant. Let me be very clear: I understand that there may be some frustration, but personal attacks, whether they involve politics, nationality, or anything else, are not allowed here. In order to have a healthy exchange, I quickly put a review before this PR so that we can discuss and understand the project's challenges to find proper solutions. I could have completely set the feature aside, but instead I spent time, that I could have used elsewhere, because I think it's worth it.
If you have created a file manager app that solves every file synchronization issues by managing multiple protocols independently and interoperably for any type of file even with blob, while providing a satisfying user experience, in one day, it's really really cool and I would be curious to see it. Once again, I am not limiting new ways of creation, as they can be very helpful and allow for rapid execution. I thank you for the PR, that's very interesting, but please understand that I have to conduct a thorough study before rolling it out to all users. I don't have much room for error, and of course I'm the one who has to play the "bad guy" and who must thoroughly check. I'm not thrilled about it and I don't like that but it's necessary and healthy. Architectural choices of KeePassDX must simply remain consistent. And instead of arguing about the hybrid architecture choices you made, you emphasize the speed of execution and make personal attacks, why? The problem has never been generating code that works, but rather code that works and is properly structured and designed for long-term maintenance. If you are certain that your architecture is sound and you are prepared to perform maintenance for years to come and manage future friction that you did not anticipate when accepting the code, you already have your fork, so it's a win-win for you, and that's the whole point of open source. I haven't looked at the new code yet, I'll put the actual review in version 4.5.0. https://github.com/Kunzisoft/KeePassDX/projects?query=is%3Aopen |
|
This PR seems to be dead on arrival unless the author (be it the AI agent or the person submitting it) can follow the basic design requirements laid out (all of which was specified in the original issue). At the bare minimum this PR needs to be split out into a few interdependent PRs:
@Lcstyle you are probably new to open source contribution and do not realize how big of a burden you are putting on others with AI generated code. If you are serious about wanting this feature and sharing it with others then be considerate and lower this burden by splitting things into smaller bits and making them more reviewable. I have had an unfortunate enough experience at my day job dealing with AI PRs and I can tell you that this PR in this current state will require at least 300 comments back-and-forth spanning probably a year. If things are split up small enough the AI is actually better at aligning with the intended direction and re-use existing code, cutting this down to maybe 20-40 comments over a few weeks to a month. @J-Jamet unfortunately this is the kind of things that we have to deal with these days. The only way that I managed to stay sane about things is to not go into details and just state the bare minimum. It helps to consider that all the effort you put in the reply often does not go to the author and it is just copy-pasted into the chatbot. For the cases where people use AI ethically the user does not just present you with AI responses, instead they properly review the code and can respond more concise with a human voice. So don't be shy about being more direct about handling these situations in ways that are more beneficial to your mental well-being. The AI chatbots are not considerate towards that. |
|
I've taken the liberty to give this a shot in #2544 . Please have a look, and let me know if splitting it up into more pr according to the individual commits would help reviewing. Disclaimer: I've used AI assistance for this, but kept being in the loop trying to understand and more importantly steer it to some solution, that seemed architecturally sound. |
Summary
{DEVICE_ID}.kdbx) into a shared sync directory, eliminating file conflicts between devicesShareObserverpattern, prevents feedback loops)DatabaseInputKDBXignoringlastModificationTimeon CustomData during KDBX parseWhat's included
DatabaseInputKDBX.ktlines 543/595 — CustomData timestamps were being discardedDesign principles
content://URIsInterop
Matching KeePassXC implementation: keepassxreboot/keepassxc#13080
Discussion: #2434
Test plan