diff --git a/android/app/src/main/java/com/labfytools/trainlog/data/BluetoothSyncTransport.kt b/android/app/src/main/java/com/labfytools/trainlog/data/BluetoothSyncTransport.kt index d07f940..66eb3d1 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/data/BluetoothSyncTransport.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/data/BluetoothSyncTransport.kt @@ -41,6 +41,17 @@ private const val MAX_BT_FILE_BYTES = 64L * 1024L * 1024L private const val MAX_BT_FILES = 128 private const val AUTO_REQUEST_COOLDOWN_MS = 5L * 60L * 1000L +internal val BLUETOOTH_ANDROID_PULL_COORDINATION = + listOf( + "android-peer-v1.json", + FULL_GENERATION_REQUEST_NAME, + LEGACY_SYNC_REQUEST_NAME, + "android-generation-v1.json", + "android-consumption-ack-v1.json", + "android-archive-acknowledgements-v1.json", + "android-generation-error-v1.json", + ) + internal data class BondedBluetoothDesktop( val name: String, val address: String, @@ -248,14 +259,7 @@ private class BluetoothExchangeArchive( val totals = longArrayOf(0L, 0L) try { ZipOutputStream(BufferedOutputStream(FileOutputStream(output))).use { zip -> - listOf( - "android-peer-v1.json", - FULL_GENERATION_REQUEST_NAME, - LEGACY_SYNC_REQUEST_NAME, - "android-generation-v1.json", - "android-consumption-ack-v1.json", - "android-generation-error-v1.json", - ).forEach { name -> + BLUETOOTH_ANDROID_PULL_COORDINATION.forEach { name -> addFile(zip, File(exchangeDirectory, name), name, seen, totals) } val reference = File(exchangeDirectory, "android-generation-v1.json") diff --git a/android/app/src/main/java/com/labfytools/trainlog/data/DriveSyncTransport.kt b/android/app/src/main/java/com/labfytools/trainlog/data/DriveSyncTransport.kt index 7e5af45..0e60015 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/data/DriveSyncTransport.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/data/DriveSyncTransport.kt @@ -195,7 +195,8 @@ internal class DriveDocumentTransport(private val context: Context, treeUri: Uri localRoot.mkdirs() val coordination = listOf( "android-peer-v1.json", "android-generation-v1.json", "android-consumption-ack-v1.json", - "android-generation-error-v1.json", "request-v1.json", + "android-archive-acknowledgements-v1.json", "android-generation-error-v1.json", + "request-v1.json", "desktop-archive-acknowledgements-v1.json", "desktop-consumption-ack-v1.json", "desktop-generation-v1.json", ) @@ -261,6 +262,13 @@ internal class DriveDocumentTransport(private val context: Context, treeUri: Uri upload("android-generation-v1.json", reference) } + fun pushRecoveryAcknowledgements(localRoot: File) { + upload( + "android-archive-acknowledgements-v1.json", + File(localRoot, "android-archive-acknowledgements-v1.json"), + ) + } + fun pushAcknowledgement(localRoot: File) { upload( "android-consumption-ack-v1.json", @@ -318,6 +326,10 @@ class DriveSyncWorker(context: Context, parameters: WorkerParameters) : local, Duration.ofMinutes(10), afterPeerPublication = { transport.pushPeer(local) }, + afterRecoveryAcknowledgements = { + stage = "recovery-acknowledgement" + transport.pushRecoveryAcknowledgements(local) + }, afterPublication = { stage = "generation-publication" transport.pushGeneration(local) diff --git a/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationForegroundCoordinator.kt b/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationForegroundCoordinator.kt index 009dd5e..2f6e18f 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationForegroundCoordinator.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationForegroundCoordinator.kt @@ -404,6 +404,7 @@ internal class SyncGenerationCoordinator( directory: File, timeout: Duration = Duration.ofMinutes(5), afterPeerPublication: (() -> Unit)? = null, + afterRecoveryAcknowledgements: (() -> Unit)? = null, afterPublication: (() -> Unit)? = null, pollTransport: (() -> Unit)? = null, afterAcknowledgement: (() -> Unit)? = null, @@ -509,6 +510,7 @@ internal class SyncGenerationCoordinator( ) publish(recoveryPath, recoveryEnvelope) visibility.confirm(listOf(recoveryPath)) + afterRecoveryAcknowledgements?.invoke() phase( runId, "android_archive_ack_published", diff --git a/android/app/src/test/java/com/labfytools/trainlog/data/BluetoothSyncTransportContractTest.kt b/android/app/src/test/java/com/labfytools/trainlog/data/BluetoothSyncTransportContractTest.kt new file mode 100644 index 0000000..55aef0c --- /dev/null +++ b/android/app/src/test/java/com/labfytools/trainlog/data/BluetoothSyncTransportContractTest.kt @@ -0,0 +1,15 @@ +package com.labfytools.trainlog.data + +import org.junit.Assert.assertTrue +import org.junit.Test + +class BluetoothSyncTransportContractTest { + @Test + fun androidPullArchiveCarriesGenerationAckRecoveryEvidence() { + assertTrue( + BLUETOOTH_ANDROID_PULL_COORDINATION.contains( + "android-archive-acknowledgements-v1.json" + ) + ) + } +} diff --git a/tests/test_sync_drive_transport.py b/tests/test_sync_drive_transport.py index 2cf5aee..0eaf740 100644 --- a/tests/test_sync_drive_transport.py +++ b/tests/test_sync_drive_transport.py @@ -116,6 +116,8 @@ class DriveTransportTest(unittest.TestCase): source = Path(raw) / "source" source.mkdir() self.generation(source) + recovery = source / "android-archive-acknowledgements-v1.json" + recovery.write_text('{"format":"trainlog-sync-archive-acknowledgements","version":1}') remote = MemoryDrive() drive.push(remote, source) destination = Path(raw) / "destination" @@ -124,6 +126,10 @@ class DriveTransportTest(unittest.TestCase): (destination / "desktop-objects/generations/gen_one/history.json").read_bytes(), b"{}\n", ) + self.assertEqual( + (destination / "android-archive-acknowledgements-v1.json").read_bytes(), + recovery.read_bytes(), + ) remote.downloads.clear() drive.pull(remote, destination) diff --git a/tools/sync_drive_transport.py b/tools/sync_drive_transport.py index 8e39806..47e0c4f 100644 --- a/tools/sync_drive_transport.py +++ b/tools/sync_drive_transport.py @@ -20,6 +20,7 @@ COORDINATION = ( "android-peer-v1.json", "android-generation-v1.json", "android-consumption-ack-v1.json", + "android-archive-acknowledgements-v1.json", "android-generation-error-v1.json", "request-v1.json", "desktop-archive-acknowledgements-v1.json", diff --git a/tools/sync_orchestrator.py b/tools/sync_orchestrator.py index 165ad52..ae87b5e 100644 --- a/tools/sync_orchestrator.py +++ b/tools/sync_orchestrator.py @@ -242,6 +242,7 @@ def mirror_current_exchange(transport_root: Path, remote: str, timeout: int) -> "android-peer-v1.json", "android-generation-v1.json", "android-consumption-ack-v1.json", + "android-archive-acknowledgements-v1.json", "android-generation-error-v1.json", "request-v1.json", "desktop-archive-acknowledgements-v1.json", diff --git a/tui/src/generation_mtp.c b/tui/src/generation_mtp.c index b030183..26c6389 100644 --- a/tui/src/generation_mtp.c +++ b/tui/src/generation_mtp.c @@ -622,6 +622,14 @@ TrainlogStatus trainlog_generation_mtp_pull(const TrainlogGenerationMtpIo *io, status = pull_named_file( io, &selection, selection.root, "android-consumption-ack-v1.json", local, false); } + if (status == TRAINLOG_STATUS_OK) { + status = pull_named_file(io, + &selection, + selection.root, + "android-archive-acknowledgements-v1.json", + local, + false); + } if (status == TRAINLOG_STATUS_OK) { status = pull_named_file( io, &selection, selection.root, "android-generation-error-v1.json", local, false); diff --git a/tui/tests/test_generation_mtp.c b/tui/tests/test_generation_mtp.c index 1de7bf7..c3bddbc 100644 --- a/tui/tests/test_generation_mtp.c +++ b/tui/tests/test_generation_mtp.c @@ -250,6 +250,12 @@ static int test_pull_and_manifest_last(void) { "trainlog-sync-full-generation-request-v1.json", "{\"format\":\"trainlog-sync-request\",\"version\":1,\"request_id\":\"sr_11111111-" "1111-4111-8111-111111111111\",\"requested_at\":\"2026-09-20T12:00:00Z\"}"); + add(15, + 2, + false, + "android-archive-acknowledgements-v1.json", + "{\"format\":\"trainlog-sync-archive-acknowledgements\",\"version\":1," + "\"acknowledgements\":[]}"); add(5, 2, true, "android-objects", NULL); add(6, 5, true, "generations", NULL); add(7, 6, true, "gen_11111111-1111-4111-8111-111111111111", NULL); @@ -272,6 +278,9 @@ static int test_pull_and_manifest_last(void) { CHECK(access(path, R_OK) == 0); CHECK(trainlog_test_join_path(path, sizeof(path), root, "trainlog-sync-request-v1.json")); CHECK(access(path, R_OK) == 0); + CHECK(trainlog_test_join_path( + path, sizeof(path), root, "android-archive-acknowledgements-v1.json")); + CHECK(access(path, R_OK) == 0); CHECK(trainlog_test_join_path(path, sizeof(path), root, "out")); CHECK(mkdir(path, 0700) == 0); char gen[512];