Skip to content

Auto-upload offline-logged QSOs when connectivity returns#348

Merged
patrickrb merged 2 commits into
devfrom
feat/qso-auto-sync
Jun 26, 2026
Merged

Auto-upload offline-logged QSOs when connectivity returns#348
patrickrb merged 2 commits into
devfrom
feat/qso-auto-sync

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Problem

If a user logs a QSO with no internet, the contact saves locally fine but the immediate QRZ/Cloudlog upload silently fails — the row's synced_cloudlog/synced_qrz flags stay 0. The only recovery today is remembering to open the logbook and press the manual Sync button.

Fix

The QRZ/Cloudlog path already has the full persistence foundation (per-QSO sync flags + a working catch-up uploader ThirdPartyService.syncAllQSOs). The only missing piece was an automatic trigger.

This adds a lightweight ConnectivityManager.NetworkCallback (QsoAutoSync) that re-runs the catch-up uploader the moment a network with internet becomes available, plus once on app start (covers a QSO logged offline before the app was last closed).

  • QsoSyncGate (pure, unit-tested): single-flight so two triggers can't launch concurrent upload passes, and a 15 s debounce so a flapping connection can't spam the services.
  • ThirdPartyService.countUnsyncedQSOs: shares one unsyncedFilter() with syncAllQSOs, so the auto-sync skips spawning work when nothing is pending.
  • Wired into ComposeMainActivity alongside the existing USB/Bluetooth receivers (register + syncNow("app-start") in onCreate, unregister in onDestroy).

Scope: QRZ + Cloudlog only (the services with per-QSO sync flags). POTA stays manual. No new dependency — fits the existing no-WorkManager codebase. The immediate post-QSO upload path is unchanged; offline failures correctly leave the flags at 0 for the auto-sync to pick up.

Trade-off: does not cover the app being killed and staying killed until internet returns — the next launch catches that up via the app-start flush.

Tests

  • QsoSyncGateTest (pure JUnit): single-flight, debounce window, debounce-measured-from-start, service-disabled gating.
  • ThirdPartyServiceUnsyncedCountTest (Robolectric + in-memory SQLite): count matches the syncAllQSOs filter across each enable-flag combination; 0 when both disabled or db null.

Both pass: gradlew.bat testDebugUnitTest --tests *QsoSyncGateTest --tests *ThirdPartyServiceUnsyncedCountTest → BUILD SUCCESSFUL. Full installDebug APK builds clean (native libs + packaging + signing).

Not yet verified on-device

No phone/emulator was connected at commit time, so the manual end-to-end (airplane mode → log QSO → re-enable network → confirm synced_* flip to 1 and debug.log shows the QsoAutoSync lines) still needs a device run.

🤖 Generated with Claude Code

When a QSO is logged with no internet, the immediate QRZ/Cloudlog upload
fails and the row's synced_cloudlog/synced_qrz flags stay 0. Until now the
only recovery was the manual Sync button in the logbook.

Add a lightweight ConnectivityManager listener (QsoAutoSync) that re-runs the
existing catch-up uploader (ThirdPartyService.syncAllQSOs) the moment a
network with internet becomes available, plus once on app start. A small
single-flight + debounce gate (QsoSyncGate) keeps a flapping connection from
spawning concurrent or rapid-fire upload passes, and countUnsyncedQSOs skips
the work entirely when nothing is pending.

Scope is QRZ + Cloudlog (the services with per-QSO sync flags); POTA stays
manual. No new dependency — fits the existing no-WorkManager codebase.

- AndroidManifest: add ACCESS_NETWORK_STATE
- ThirdPartyService: extract shared unsyncedFilter(), add countUnsyncedQSOs()
- ComposeMainActivity: register/syncNow in onCreate, unregister in onDestroy
- Tests: QsoSyncGateTest (pure), ThirdPartyServiceUnsyncedCountTest (Robolectric)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.06250% with 55 lines in your changes missing coverage. Please review.
✅ Project coverage is 11.48%. Comparing base (6bc083c) to head (23796a2).
⚠️ Report is 29 commits behind head on dev.

Files with missing lines Patch % Lines
...main/kotlin/radio/ks3ckc/ft8af/sync/QsoAutoSync.kt 15.00% 51 Missing ⚠️
...n/kotlin/radio/ks3ckc/ft8af/ComposeMainActivity.kt 0.00% 4 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##                dev     #348      +/-   ##
============================================
- Coverage     11.70%   11.48%   -0.23%     
- Complexity      113      120       +7     
============================================
  Files            85       90       +5     
  Lines         11691    12628     +937     
  Branches       2110     2260     +150     
============================================
+ Hits           1369     1450      +81     
- Misses        10191    11047     +856     
  Partials        131      131              
Files with missing lines Coverage Δ
...n/kotlin/radio/ks3ckc/ft8af/ComposeMainActivity.kt 2.47% <0.00%> (-0.11%) ⬇️
...main/kotlin/radio/ks3ckc/ft8af/sync/QsoAutoSync.kt 15.00% <15.00%> (ø)

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an automatic catch-up sync for QSOs that were logged while offline, so QRZ/Cloudlog uploads resume as soon as connectivity returns (and once on app start), reducing reliance on the manual Sync button.

Changes:

  • Introduces QsoAutoSync (ConnectivityManager callback + background sync trigger) and QsoSyncGate (single-flight + debounce).
  • Adds ThirdPartyService.countUnsyncedQSOs() sharing a single unsyncedFilter() with syncAllQSOs() to avoid launching work when nothing is pending.
  • Wires auto-sync into ComposeMainActivity lifecycle and adds unit/Robolectric coverage for the new gating + count logic.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/sync/QsoAutoSync.kt Adds connectivity-triggered/app-start auto-sync and gating logic for offline-upload recovery.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ComposeMainActivity.kt Registers/unregisters the auto-sync component and triggers an app-start flush.
ft8af/app/src/main/java/com/k1af/ft8af/log/ThirdPartyService.java Extracts shared unsynced WHERE filter and adds an unsynced-row count API used by auto-sync.
ft8af/app/src/main/AndroidManifest.xml Adds ACCESS_NETWORK_STATE permission needed for connectivity observation.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/sync/QsoSyncGateTest.kt Unit tests for the single-flight + debounce gate behavior.
ft8af/app/src/test/kotlin/com/k1af/ft8af/log/ThirdPartyServiceUnsyncedCountTest.kt Robolectric test validating unsynced counting matches the sync filter logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/sync/QsoAutoSync.kt
Comment thread ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/sync/QsoAutoSync.kt
Comment thread ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/sync/QsoAutoSync.kt Outdated
Address review feedback on the auto-sync wiring:
- Require NET_CAPABILITY_VALIDATED (not just INTERNET) so sync doesn't fire
  on captive-portal / still-validating networks and churn failed uploads.
- Use SystemClock.elapsedRealtime() for the debounce interval instead of
  System.currentTimeMillis(), so NTP/manual time changes can't wedge the gate.
- Pass the application Context and "data.db" to DatabaseOpr.getInstance()
  instead of (null, null), so a cold-start trigger that beats MainViewModel's
  init can't construct the singleton with a null Context (NPE) or null name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@patrickrb patrickrb merged commit 1924c08 into dev Jun 26, 2026
16 checks passed
@patrickrb patrickrb deleted the feat/qso-auto-sync branch June 26, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants