@@ -41,7 +41,9 @@ class KeeShareSyncRunnable(
4141 database : ContextualDatabase ,
4242 saveDatabase : Boolean ,
4343 challengeResponseRetriever : (HardwareKey , ByteArray? ) -> ByteArray ,
44- private val progressTaskUpdater : ProgressTaskUpdater ?
44+ private val progressTaskUpdater : ProgressTaskUpdater ? ,
45+ private val silentSync : Boolean = false ,
46+ private val importOnly : Boolean = false
4547) : SaveDatabaseRunnable(
4648 context,
4749 database,
@@ -55,23 +57,49 @@ class KeeShareSyncRunnable(
5557 var exportedEntries: Int = 0
5658
5759 override fun onStartRun () {
58- database.wasReloaded = true
60+ // For silent background syncs, only reload UI later if entries actually changed
61+ if (! silentSync) {
62+ database.wasReloaded = true
63+ }
5964 super .onStartRun()
6065 }
6166
6267 override fun onActionRun () {
68+ Log .d(TAG , " === KeeShare sync onActionRun() START ===" )
6369 try {
6470 val kdbx = database.databaseKDBX
6571 if (kdbx == null ) {
72+ Log .e(TAG , " databaseKDBX is null — not a KDBX database" )
6673 setError(" KeeShare sync requires a KDBX database" )
6774 return
6875 }
76+ Log .d(TAG , " Database is KDBX, rootGroup=${kdbx.rootGroup?.title} " )
6977
7078 val deviceId = resolveDeviceId(context)
79+ Log .d(TAG , " Device ID: $deviceId " )
7180 val cacheDir = File (context.cacheDir, " keeshare" )
7281 cacheDir.mkdirs()
7382
83+ // Auto-provision PerDeviceSync from preferences sync folder + classic reference
84+ val prefSyncFolder = PreferencesUtil .getKeeShareSyncFolderUri(context)
85+ Log .d(TAG , " Preferences sync folder URI: ${prefSyncFolder ? : " (not set)" } " )
86+ if (! prefSyncFolder.isNullOrEmpty()) {
87+ provisionPerDeviceConfig(kdbx, prefSyncFolder)
88+ } else {
89+ Log .w(TAG , " No sync folder configured in preferences — auto-provisioning skipped" )
90+ }
91+
92+ // Log KeeShare config presence per group
93+ kdbx.rootGroup?.let { root ->
94+ val hasClassic = root.customData.get(com.kunzisoft.keepass.database.keeshare.KeeShareReference .CLASSIC_KEY ) != null
95+ val hasPerDev = root.customData.get(com.kunzisoft.keepass.database.keeshare.KeeShareReference .PER_DEVICE_KEY ) != null
96+ if (hasClassic || hasPerDev) {
97+ Log .d(TAG , " Root '${root.title} ': classic=$hasClassic , perDevice=$hasPerDev " )
98+ }
99+ }
100+
74101 // 1. Import from all other device containers via SAF
102+ Log .d(TAG , " --- Starting import phase ---" )
75103 val importResults = KeeShareImport .importAll(
76104 database = kdbx,
77105 ownDeviceId = deviceId,
@@ -87,28 +115,49 @@ class KeeShareSyncRunnable(
87115 importedEntries = importResults.filter { it.success }.sumOf { it.entriesImported }
88116 importedDevices = importResults.filter { it.success }
89117 .map { it.containerName }.distinct().size
118+ Log .d(TAG , " Import results: ${importResults.size} total, $importedEntries entries from $importedDevices devices" )
119+ for (r in importResults) {
120+ Log .d(TAG , " Import: group='${r.groupName} ' container='${r.containerName} ' entries=${r.entriesImported} success=${r.success} error=${r.errorMessage} " )
121+ }
122+
123+ // For silent syncs, only trigger UI reload if something was actually imported
124+ if (silentSync && importedEntries > 0 ) {
125+ database.wasReloaded = true
126+ }
90127
91128 // 2. Export own container for each shared group via SAF
92- val exportResults = KeeShareExport .exportAll(
93- database = kdbx,
94- deviceId = deviceId,
95- cacheDirectory = cacheDir,
96- targetStreamProvider = { syncDirUri, devId ->
97- openTargetOutputStream(context, syncDirUri, devId)
129+ // Skip export when triggered by auto-sync (importOnly) to prevent
130+ // feedback loops between devices via Syncthing
131+ if (! importOnly) {
132+ Log .d(TAG , " --- Starting export phase ---" )
133+ val exportResults = KeeShareExport .exportAll(
134+ database = kdbx,
135+ deviceId = deviceId,
136+ cacheDirectory = cacheDir,
137+ targetStreamProvider = { syncDirUri, devId ->
138+ openTargetOutputStream(context, syncDirUri, devId)
139+ }
140+ )
141+
142+ exportedEntries = exportResults.filter { it.success }.sumOf { it.entriesExported }
143+ Log .d(TAG , " Export results: ${exportResults.size} total, $exportedEntries entries exported" )
144+ for (r in exportResults) {
145+ Log .d(TAG , " Export: group='${r.groupName} ' entries=${r.entriesExported} success=${r.success} error=${r.errorMessage} " )
98146 }
99- )
100147
101- exportedEntries = exportResults.filter { it.success }.sumOf { it.entriesExported }
148+ val failedExports = exportResults.filter { ! it.success }
149+ if (failedExports.isNotEmpty()) {
150+ Log .w(TAG , " Failed exports: ${failedExports.map { " ${it.groupName} : ${it.errorMessage} " }} " )
151+ }
152+ } else {
153+ Log .d(TAG , " --- Export phase SKIPPED (importOnly=true) ---" )
154+ }
102155
103- // Log results
156+ // Log failed imports
104157 val failedImports = importResults.filter { ! it.success }
105- val failedExports = exportResults.filter { ! it.success }
106158 if (failedImports.isNotEmpty()) {
107159 Log .w(TAG , " Failed imports: ${failedImports.map { " ${it.containerName} : ${it.errorMessage} " }} " )
108160 }
109- if (failedExports.isNotEmpty()) {
110- Log .w(TAG , " Failed exports: ${failedExports.map { " ${it.groupName} : ${it.errorMessage} " }} " )
111- }
112161
113162 // Update last sync time
114163 PreferencesUtil .setKeeShareLastSyncTime(context, System .currentTimeMillis())
@@ -134,6 +183,57 @@ class KeeShareSyncRunnable(
134183 const val RESULT_IMPORTED_DEVICES = " KEESHARE_IMPORTED_DEVICES"
135184 const val RESULT_EXPORTED_ENTRIES = " KEESHARE_EXPORTED_ENTRIES"
136185
186+ /* *
187+ * For groups that have a classic KeeShare/Reference (from KeePassXC) with
188+ * per-device mode but no KeeShare/PerDeviceSync custom data, auto-create
189+ * the PerDeviceSync config using the sync folder URI from preferences.
190+ */
191+ private fun provisionPerDeviceConfig (
192+ database : com.kunzisoft.keepass.database.element.database.DatabaseKDBX ,
193+ syncFolderUri : String
194+ ) {
195+ fun checkGroup (group : com.kunzisoft.keepass.database.element.group.GroupKDBX ) {
196+ // Skip if already has PerDeviceSync
197+ if (group.customData.get(com.kunzisoft.keepass.database.keeshare.KeeShareReference .PER_DEVICE_KEY ) != null ) return
198+
199+ val classicData = group.customData.get(
200+ com.kunzisoft.keepass.database.keeshare.KeeShareReference .CLASSIC_KEY
201+ ) ? : return
202+ val ref = com.kunzisoft.keepass.database.keeshare.KeeShareReference
203+ .fromClassicCustomData(classicData.value) ? : return
204+
205+ if (! ref.isPerDeviceMode()) return
206+ if (ref.type != com.kunzisoft.keepass.database.keeshare.KeeShareReference .Type .SYNCHRONIZE
207+ && ref.type != com.kunzisoft.keepass.database.keeshare.KeeShareReference .Type .IMPORT ) return
208+
209+ val config = PerDeviceSyncConfig (
210+ syncDir = syncFolderUri,
211+ password = ref.password,
212+ keepGroups = ref.keepGroups
213+ )
214+ val encoded = PerDeviceSyncConfig .toCustomData(config)
215+ group.customData.put(
216+ com.kunzisoft.keepass.database.element.CustomDataItem (
217+ com.kunzisoft.keepass.database.keeshare.KeeShareReference .PER_DEVICE_KEY ,
218+ encoded,
219+ com.kunzisoft.keepass.database.element.DateInstant ()
220+ )
221+ )
222+ Log .i(TAG , " Auto-provisioned PerDeviceSync for group '${group.title} ' with folder: $syncFolderUri " )
223+ }
224+
225+ database.rootGroup?.let { checkGroup(it) }
226+ database.rootGroup?.doForEachChild(
227+ null ,
228+ object : com.kunzisoft.keepass.database.element.node.NodeHandler < com.kunzisoft.keepass.database.element.group.GroupKDBX > () {
229+ override fun operate (node : com.kunzisoft.keepass.database.element.group.GroupKDBX ): Boolean {
230+ checkGroup(node)
231+ return true
232+ }
233+ }
234+ )
235+ }
236+
137237 /* *
138238 * Resolve the device ID for this device.
139239 *
@@ -160,21 +260,27 @@ class KeeShareSyncRunnable(
160260 syncDirUri : String ,
161261 ownDeviceId : String
162262 ): List <Pair <String , InputStream >> {
263+ Log .d(TAG , " listOtherDeviceStreams: syncDirUri=$syncDirUri , ownDeviceId=$ownDeviceId " )
163264 val treeUri = try {
164265 Uri .parse(syncDirUri)
165266 } catch (e: Exception ) {
166267 Log .w(TAG , " Invalid sync dir URI: $syncDirUri " , e)
167268 return emptyList()
168269 }
270+ Log .d(TAG , " Parsed URI: $treeUri (scheme=${treeUri.scheme} )" )
169271
170272 val dir = DocumentFile .fromTreeUri(context, treeUri)
273+ Log .d(TAG , " DocumentFile: exists=${dir?.exists()} , isDir=${dir?.isDirectory} , name=${dir?.name} " )
171274 if (dir == null || ! dir.exists()) {
172275 Log .d(TAG , " Sync dir not accessible: $syncDirUri " )
173276 return emptyList()
174277 }
175278
279+ val allFiles = dir.listFiles()
280+ Log .d(TAG , " Directory contains ${allFiles.size} files: ${allFiles.map { it.name }} " )
176281 val ownFileName = PerDeviceSyncConfig .containerFileName(ownDeviceId)
177- return dir.listFiles()
282+ Log .d(TAG , " Own filename to skip: $ownFileName " )
283+ return allFiles
178284 .filter { doc ->
179285 doc.isFile
180286 && doc.name?.endsWith(" .kdbx" , ignoreCase = true ) == true
0 commit comments