From d90bc371eca4c740afc6b117e03070f42f994856 Mon Sep 17 00:00:00 2001 From: Cygnus Date: Tue, 27 Jan 2026 02:01:12 +0000 Subject: [PATCH 1/2] Fix backup/restore missing relay (COLUMBA-3) Fix relay selection not working after backup restore by ensuring relay information is properly exported and imported. Changes: - Add isMyRelay field to ContactExport (nullable for backward compatibility) - Export isMyRelay field in MigrationExporter.exportContactsForIdentity() - Restore isMyRelay field during import in MigrationImporter.importContacts() - Restore manualPropagationNode setting when relay contact is imported - Disable auto-select when manual relay is restored - Trigger auto-selection after import if enabled but no relay was restored - Inject PropagationNodeManager into MigrationImporter for auto-selection This ensures that after restoring from backup: 1. The relay contact is properly marked with isMyRelay=true 2. The manual propagation node preference is restored 3. Auto-selection is triggered if enabled but no relay was restored Fixes the issue where no propagation node would be selected after backup restore, requiring manual re-configuration. --- .../lxmf/messenger/migration/MigrationData.kt | 2 + .../messenger/migration/MigrationExporter.kt | 1 + .../messenger/migration/MigrationImporter.kt | 43 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/app/src/main/java/com/lxmf/messenger/migration/MigrationData.kt b/app/src/main/java/com/lxmf/messenger/migration/MigrationData.kt index bce412936..8bd1ab87d 100644 --- a/app/src/main/java/com/lxmf/messenger/migration/MigrationData.kt +++ b/app/src/main/java/com/lxmf/messenger/migration/MigrationData.kt @@ -99,6 +99,8 @@ data class ContactExport( val isPinned: Boolean, /** Contact status: ACTIVE, PENDING_IDENTITY, or UNRESOLVED (nullable for backward compatibility) */ val status: String? = null, + /** Whether this contact is the user's selected propagation node relay (nullable for backward compatibility) */ + val isMyRelay: Boolean? = null, ) /** diff --git a/app/src/main/java/com/lxmf/messenger/migration/MigrationExporter.kt b/app/src/main/java/com/lxmf/messenger/migration/MigrationExporter.kt index d961d5922..569f48300 100644 --- a/app/src/main/java/com/lxmf/messenger/migration/MigrationExporter.kt +++ b/app/src/main/java/com/lxmf/messenger/migration/MigrationExporter.kt @@ -208,6 +208,7 @@ class MigrationExporter lastInteractionTimestamp = contact.lastInteractionTimestamp, isPinned = contact.isPinned, status = contact.status.name, + isMyRelay = contact.isMyRelay, ) } } diff --git a/app/src/main/java/com/lxmf/messenger/migration/MigrationImporter.kt b/app/src/main/java/com/lxmf/messenger/migration/MigrationImporter.kt index bff6b2b1c..2b3f68e66 100644 --- a/app/src/main/java/com/lxmf/messenger/migration/MigrationImporter.kt +++ b/app/src/main/java/com/lxmf/messenger/migration/MigrationImporter.kt @@ -19,6 +19,7 @@ import com.lxmf.messenger.data.db.entity.PeerIdentityEntity import com.lxmf.messenger.data.model.InterfaceType import com.lxmf.messenger.repository.SettingsRepository import com.lxmf.messenger.reticulum.protocol.ReticulumProtocol +import com.lxmf.messenger.service.PropagationNodeManager import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first @@ -43,6 +44,7 @@ class MigrationImporter private val interfaceDatabase: InterfaceDatabase, private val reticulumProtocol: ReticulumProtocol, private val settingsRepository: SettingsRepository, + private val propagationNodeManager: PropagationNodeManager, ) { companion object { private const val TAG = "MigrationImporter" @@ -142,6 +144,23 @@ class MigrationImporter onProgress(0.92f) importSettings(bundle.settings, txResult.themeIdMap) + onProgress(0.95f) + + // Trigger auto-selection after import if enabled and no relay was restored + try { + val isAutoSelect = settingsRepository.getAutoSelectPropagationNode() + val manualRelay = settingsRepository.getManualPropagationNode() + + if (isAutoSelect && manualRelay == null) { + Log.d(TAG, "Auto-selection enabled but no relay restored - triggering auto-selection") + propagationNodeManager.enableAutoSelect() + } else if (!isAutoSelect && manualRelay != null) { + Log.d(TAG, "Restored manual relay: $manualRelay") + } + } catch (e: Exception) { + Log.w(TAG, "Failed to trigger auto-selection after import", e) + // Non-fatal - continue with import completion + } onProgress(1.0f) Log.i(TAG, "Migration import complete") @@ -302,6 +321,9 @@ class MigrationImporter } private suspend fun importContacts(contacts: List): Int { + // Track relay restoration for settings update + var restoredRelayHash: String? = null + val entities = contacts.map { contact -> // Determine status: use exported value, or infer from publicKey for backward compatibility @@ -318,6 +340,13 @@ class MigrationImporter ContactStatus.ACTIVE } + // Track which contact was the relay for settings restoration + val isMyRelay = contact.isMyRelay == true + if (isMyRelay) { + restoredRelayHash = contact.destinationHash + Log.d(TAG, "Restored relay contact: ${contact.customNickname ?: contact.destinationHash.take(12)}") + } + ContactEntity( destinationHash = contact.destinationHash, identityHash = contact.identityHash, @@ -330,10 +359,24 @@ class MigrationImporter lastInteractionTimestamp = contact.lastInteractionTimestamp, isPinned = contact.isPinned, status = status, + isMyRelay = isMyRelay, ) } database.contactDao().insertContacts(entities) Log.d(TAG, "Imported ${entities.size} contacts") + + // Restore manual propagation node setting if a relay was restored + if (restoredRelayHash != null) { + try { + settingsRepository.saveManualPropagationNode(restoredRelayHash) + // If we restored a relay, the user had manual selection enabled + settingsRepository.saveAutoSelectPropagationNode(false) + Log.d(TAG, "Restored manual propagation node setting: $restoredRelayHash") + } catch (e: Exception) { + Log.e(TAG, "Failed to restore propagation node settings", e) + } + } + return entities.size } From a39b25582f22716a78f7756e051c2b7f1658ba78 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Tue, 27 Jan 2026 19:14:42 -0500 Subject: [PATCH 2/2] refactor: move relay DataStore writes outside Room transaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract restoreRelaySettings() to run after both the DB transaction and importSettings(), ensuring DataStore writes are never inside a Room transaction scope (prevents inconsistent state on rollback) - Return relay hash through ContactImportResult → TransactionResult instead of writing to DataStore inside importContacts() - Check if importSettings already restored the relay before writing from the contact's isMyRelay flag, removing redundant overwrites - Add warning log when multiple relay contacts found in backup Co-Authored-By: Claude Opus 4.5 --- .../messenger/migration/MigrationImporter.kt | 104 ++++++++++++------ 1 file changed, 69 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/lxmf/messenger/migration/MigrationImporter.kt b/app/src/main/java/com/lxmf/messenger/migration/MigrationImporter.kt index 2b3f68e66..6ec673e12 100644 --- a/app/src/main/java/com/lxmf/messenger/migration/MigrationImporter.kt +++ b/app/src/main/java/com/lxmf/messenger/migration/MigrationImporter.kt @@ -146,21 +146,9 @@ class MigrationImporter importSettings(bundle.settings, txResult.themeIdMap) onProgress(0.95f) - // Trigger auto-selection after import if enabled and no relay was restored - try { - val isAutoSelect = settingsRepository.getAutoSelectPropagationNode() - val manualRelay = settingsRepository.getManualPropagationNode() - - if (isAutoSelect && manualRelay == null) { - Log.d(TAG, "Auto-selection enabled but no relay restored - triggering auto-selection") - propagationNodeManager.enableAutoSelect() - } else if (!isAutoSelect && manualRelay != null) { - Log.d(TAG, "Restored manual relay: $manualRelay") - } - } catch (e: Exception) { - Log.w(TAG, "Failed to trigger auto-selection after import", e) - // Non-fatal - continue with import completion - } + // Restore relay settings after both the DB transaction and settings import + // so DataStore writes are never inside a Room transaction scope. + restoreRelaySettings(txResult.restoredRelayHash) onProgress(1.0f) Log.i(TAG, "Migration import complete") @@ -190,6 +178,8 @@ class MigrationImporter val peerIdentitiesImported: Int, val customThemesImported: Int, val themeIdMap: Map, + /** Destination hash of the relay contact restored from backup, if any. */ + val restoredRelayHash: String?, ) /** @@ -226,7 +216,7 @@ class MigrationImporter it.identityHash in importedIdentityHashes || database.localIdentityDao().identityExists(it.identityHash) } - val contacts = importContacts(validContacts) + val contactResult = importContacts(validContacts) onProgress(0.75f) val announces = importAnnounces(bundle.announces) @@ -238,7 +228,16 @@ class MigrationImporter val (themes, idMap) = importCustomThemes(bundle.customThemes) onProgress(0.82f) - return TransactionResult(identities, messages, contacts, announces, peerIdentities, themes, idMap) + return TransactionResult( + identities, + messages, + contactResult.imported, + announces, + peerIdentities, + themes, + idMap, + contactResult.relayHash, + ) } private suspend fun importIdentities( @@ -320,10 +319,10 @@ class MigrationImporter return entities.size } - private suspend fun importContacts(contacts: List): Int { - // Track relay restoration for settings update + private suspend fun importContacts(contacts: List): ContactImportResult { + // Track relay restoration — written to DataStore after the transaction completes var restoredRelayHash: String? = null - + val entities = contacts.map { contact -> // Determine status: use exported value, or infer from publicKey for backward compatibility @@ -343,8 +342,11 @@ class MigrationImporter // Track which contact was the relay for settings restoration val isMyRelay = contact.isMyRelay == true if (isMyRelay) { + if (restoredRelayHash != null) { + Log.w(TAG, "Multiple relay contacts found in backup, using latest") + } restoredRelayHash = contact.destinationHash - Log.d(TAG, "Restored relay contact: ${contact.customNickname ?: contact.destinationHash.take(12)}") + Log.d(TAG, "Found relay contact: ${contact.customNickname ?: contact.destinationHash.take(12)}") } ContactEntity( @@ -364,20 +366,8 @@ class MigrationImporter } database.contactDao().insertContacts(entities) Log.d(TAG, "Imported ${entities.size} contacts") - - // Restore manual propagation node setting if a relay was restored - if (restoredRelayHash != null) { - try { - settingsRepository.saveManualPropagationNode(restoredRelayHash) - // If we restored a relay, the user had manual selection enabled - settingsRepository.saveAutoSelectPropagationNode(false) - Log.d(TAG, "Restored manual propagation node setting: $restoredRelayHash") - } catch (e: Exception) { - Log.e(TAG, "Failed to restore propagation node settings", e) - } - } - - return entities.size + + return ContactImportResult(entities.size, restoredRelayHash) } private suspend fun importAnnounces(announces: List): Int { @@ -449,6 +439,8 @@ class MigrationImporter return imported } + private data class ContactImportResult(val imported: Int, val relayHash: String?) + private data class ThemeImportResult(val imported: Int, val idMap: Map) private suspend fun importCustomThemes(themes: List): ThemeImportResult { @@ -766,4 +758,46 @@ class MigrationImporter ) { LegacySettingsImporter(settingsRepository).importAll(settings, themeIdMap) } + + /** + * Restore relay (propagation node) settings after the DB transaction and settings import. + * + * This runs AFTER importSettings, which may have already restored the relay preference + * from the backup's DataStore preferences. We only write from the contact's isMyRelay + * flag if importSettings didn't already restore a manual relay — this covers the case + * where an old backup has the contact flag but not the DataStore preference. + * + * If no relay was restored from either source and auto-select is enabled, + * trigger auto-selection so the user doesn't end up with no relay at all. + */ + private suspend fun restoreRelaySettings(restoredRelayHash: String?) { + try { + val manualRelay = settingsRepository.getManualPropagationNode() + + if (manualRelay != null) { + // importSettings already restored the relay preference — nothing to do + Log.d(TAG, "Relay already restored from settings: $manualRelay") + return + } + + if (restoredRelayHash != null) { + // Contact had isMyRelay=true but settings didn't include the preference + // (e.g., older backup format). Write it now. + settingsRepository.saveManualPropagationNode(restoredRelayHash) + settingsRepository.saveAutoSelectPropagationNode(false) + Log.d(TAG, "Restored manual propagation node from contact flag: $restoredRelayHash") + return + } + + // No relay from either source — trigger auto-select if enabled + val isAutoSelect = settingsRepository.getAutoSelectPropagationNode() + if (isAutoSelect) { + Log.d(TAG, "No relay restored, auto-select enabled — triggering auto-selection") + propagationNodeManager.enableAutoSelect() + } + } catch (e: Exception) { + Log.w(TAG, "Failed to restore relay settings after import", e) + // Non-fatal — user can manually select a relay + } + } }