feat(wavelog): per-record Wavelog upload state (model, migration, dao, repo)

This commit is contained in:
atsunatsu committed 2026-10-05 01:22:34 +08:00
1 parent cbe3e50f86
commit 733f660d78
7 files changed
+54 -2

No files matched your search

@@ -47,4 +47,7 @@ interface QsoDao {
@Query("UPDATE qso_records SET lotwUploaded = 1 WHERE id IN (:ids)")
suspend fun markUploaded(ids: List<Long>)
@Query("UPDATE qso_records SET wavelogUploaded = 1 WHERE id IN (:ids)")
suspend fun markWavelogUploaded(ids: List<Long>)
}
@@ -20,7 +20,7 @@ import com.rtbishop.look4sat.core.data.database.entity.QsoEntity
* upload receipts). Kept apart from [Look4SatDb] so existing installations
* never need a migration of the satellite database.
*/
@Database(entities = [QsoEntity::class], version = 3, exportSchema = false)
@Database(entities = [QsoEntity::class], version = 4, exportSchema = false)
abstract class QsoDatabase : RoomDatabase() {
abstract fun qsoDao(): QsoDao
}
@@ -38,3 +38,10 @@ val MIGRATION_QSO_2_3 = object : Migration(2, 3) {
db.execSQL("ALTER TABLE qso_records ADD COLUMN theirVuccGrids TEXT NOT NULL DEFAULT ''")
}
}
/** v3 → v4: track whether a QSO was uploaded to Wavelog (independent of the LoTW state). */
val MIGRATION_QSO_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE qso_records ADD COLUMN wavelogUploaded INTEGER NOT NULL DEFAULT 0")
}
}
@@ -44,6 +44,7 @@ data class QsoEntity(
@ColumnInfo(defaultValue = "''") val propagationMode: String = "",
@ColumnInfo(defaultValue = "0") val lotwConfirmed: Boolean = false,
@ColumnInfo(defaultValue = "0") val lotwUploaded: Boolean = false,
@ColumnInfo(defaultValue = "0") val wavelogUploaded: Boolean = false,
@ColumnInfo(defaultValue = "0") val lotwReceived: Boolean = false,
@ColumnInfo(defaultValue = "''") val lotwQslDate: String = "",
@ColumnInfo(defaultValue = "''") val vuccGrids: String = "",
@@ -51,12 +51,14 @@ class QsoRepository(
// place: that contact already exists on LoTW under the old values. The original row
// keeps its upload/confirmation state, and the edited content is saved as a NEW
// record (without any LoTW state, so it can be uploaded again).
if (previous != null && (previous.lotwUploaded || previous.lotwConfirmed) &&
if (previous != null &&
(previous.lotwUploaded || previous.lotwConfirmed || previous.wavelogUploaded) &&
!record.sameEditableContentAs(previous)
) {
dao.save(record.copy(
id = 0L,
lotwUploaded = false, lotwConfirmed = false, lotwReceived = false,
wavelogUploaded = false,
lotwQslDate = "", vuccGrids = emptyList(), theirVuccGrids = emptyList(),
dxcc = null, country = "", cqZone = null, region = ""
).toEntity())
@@ -93,6 +95,11 @@ class QsoRepository(
if (stamped.isNotEmpty()) dao.saveBatch(stamped.map { it.toEntity() })
}
override suspend fun markWavelogUploaded(ids: List<Long>) = withContext(dispatcher) {
if (ids.isEmpty()) return@withContext
importMutex.withLock { dao.markWavelogUploaded(ids) }
}
override suspend fun exportAdi(ids: Set<Long>?, includeIncomplete: Boolean): String = withContext(dispatcher) {
val records = dao.getAll()
.asSequence()
@@ -286,6 +293,7 @@ private fun QsoEntity.toDomain() = QsoRecord(
propagationMode = propagationMode,
lotwConfirmed = lotwConfirmed,
lotwUploaded = lotwUploaded,
wavelogUploaded = wavelogUploaded,
lotwReceived = lotwReceived,
lotwQslDate = lotwQslDate,
vuccGrids = vuccGrids.split(',').filter(String::isNotBlank),
@@ -323,6 +331,7 @@ private fun QsoRecord.toEntity() = QsoEntity(
propagationMode = propagationMode.ifBlank { if (satelliteName.isNotBlank()) "SAT" else "" },
lotwConfirmed = lotwConfirmed,
lotwUploaded = lotwUploaded,
wavelogUploaded = wavelogUploaded,
lotwReceived = lotwReceived,
lotwQslDate = lotwQslDate,
vuccGrids = vuccGrids.joinToString(","),
@@ -295,6 +295,27 @@ class QsoRepositoryTest {
assertEquals(1, dao.getAll().size)
}
@Test
fun markWavelogUploaded_setsOnlyTheWavelogFlag() = runBlocking {
val id = dao.save(loggedInApp("SO-50", lotwUploaded = false).toEntity())
repository.markWavelogUploaded(listOf(id))
val row = dao.getAll().single()
assertTrue(row.wavelogUploaded)
assertFalse(row.lotwUploaded)
}
@Test
fun save_editingAWavelogUploadedRecordCreatesANewRecord() = runBlocking {
val id = dao.save(loggedInApp("SO-50", lotwUploaded = false).copy(wavelogUploaded = true).toEntity())
repository.save(loggedInApp("SO-50", lotwUploaded = false).copy(id = id, theirCallsign = "BG5JVM"))
assertEquals(2, dao.getAll().size)
assertEquals(1, dao.getAll().count { it.wavelogUploaded })
}
/** A record as the log page creates it: tracker name, repeater pair, uplink band. */
private fun loggedInApp(satelliteName: String, lotwUploaded: Boolean) = QsoRecord(
startUtcMillis = qsoStart,
@@ -355,6 +376,7 @@ class QsoRepositoryTest {
propagationMode = propagationMode,
lotwConfirmed = lotwConfirmed,
lotwUploaded = lotwUploaded,
wavelogUploaded = wavelogUploaded,
lotwReceived = lotwReceived,
lotwQslDate = lotwQslDate,
vuccGrids = vuccGrids.joinToString(","),
@@ -393,5 +415,9 @@ class QsoRepositoryTest {
override suspend fun markUploaded(ids: List<Long>) {
ids.forEach { id -> rows[id]?.let { rows[id] = it.copy(lotwUploaded = true) } }
}
override suspend fun markWavelogUploaded(ids: List<Long>) {
ids.forEach { id -> rows[id]?.let { rows[id] = it.copy(wavelogUploaded = true) } }
}
}
}
@@ -23,6 +23,10 @@ interface IQsoRepository {
* logbook row can show which grids the QSO went out under.
*/
suspend fun markUploaded(ids: List<Long>, grids: List<String> = emptyList())
/** Marks the batch as uploaded to Wavelog (independent of the LoTW upload state). */
suspend fun markWavelogUploaded(ids: List<Long>)
suspend fun exportAdi(ids: Set<Long>? = null, includeIncomplete: Boolean = false): String
suspend fun importAdi(content: String): AdifImportResult
suspend fun mergeConfirmed(records: List<QsoRecord>): AdifImportResult
@@ -40,6 +40,8 @@ data class QsoRecord(
val propagationMode: String = if (satelliteName.isNotBlank()) "SAT" else "",
val lotwConfirmed: Boolean = false,
val lotwUploaded: Boolean = false,
/** Internal: set after a successful Wavelog upload; keeps the pending filter small. Not shown in any list. */
val wavelogUploaded: Boolean = false,
val lotwReceived: Boolean = false,
val lotwQslDate: String = "",
val vuccGrids: List<String> = emptyList(),