Auto-upload offline-logged QSOs when connectivity returns#348
Merged
Conversation
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 Report❌ Patch coverage is
Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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) andQsoSyncGate(single-flight + debounce). - Adds
ThirdPartyService.countUnsyncedQSOs()sharing a singleunsyncedFilter()withsyncAllQSOs()to avoid launching work when nothing is pending. - Wires auto-sync into
ComposeMainActivitylifecycle 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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_qrzflags stay0. 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 oneunsyncedFilter()withsyncAllQSOs, so the auto-sync skips spawning work when nothing is pending.ComposeMainActivityalongside the existing USB/Bluetooth receivers (register +syncNow("app-start")inonCreate,unregisterinonDestroy).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
0for 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 thesyncAllQSOsfilter across each enable-flag combination; 0 when both disabled or db null.Both pass:
gradlew.bat testDebugUnitTest --tests *QsoSyncGateTest --tests *ThirdPartyServiceUnsyncedCountTest→ BUILD SUCCESSFUL. FullinstallDebugAPK 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 anddebug.logshows theQsoAutoSynclines) still needs a device run.🤖 Generated with Claude Code