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)") @Query("UPDATE qso_records SET lotwUploaded = 1 WHERE id IN (:ids)")
suspend fun markUploaded(ids: List<Long>) 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 * upload receipts). Kept apart from [Look4SatDb] so existing installations
* never need a migration of the satellite database. * 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 class QsoDatabase : RoomDatabase() {
abstract fun qsoDao(): QsoDao 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 ''") 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 = "''") val propagationMode: String = "",
@ColumnInfo(defaultValue = "0") val lotwConfirmed: Boolean = false, @ColumnInfo(defaultValue = "0") val lotwConfirmed: Boolean = false,
@ColumnInfo(defaultValue = "0") val lotwUploaded: 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 = "0") val lotwReceived: Boolean = false,
@ColumnInfo(defaultValue = "''") val lotwQslDate: String = "", @ColumnInfo(defaultValue = "''") val lotwQslDate: String = "",
@ColumnInfo(defaultValue = "''") val vuccGrids: 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 // 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 // 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). // 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) !record.sameEditableContentAs(previous)
) { ) {
dao.save(record.copy( dao.save(record.copy(
id = 0L, id = 0L,
lotwUploaded = false, lotwConfirmed = false, lotwReceived = false, lotwUploaded = false, lotwConfirmed = false, lotwReceived = false,
wavelogUploaded = false,
lotwQslDate = "", vuccGrids = emptyList(), theirVuccGrids = emptyList(), lotwQslDate = "", vuccGrids = emptyList(), theirVuccGrids = emptyList(),
dxcc = null, country = "", cqZone = null, region = "" dxcc = null, country = "", cqZone = null, region = ""
).toEntity()) ).toEntity())
@@ -93,6 +95,11 @@ class QsoRepository(
if (stamped.isNotEmpty()) dao.saveBatch(stamped.map { it.toEntity() }) 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) { override suspend fun exportAdi(ids: Set<Long>?, includeIncomplete: Boolean): String = withContext(dispatcher) {
val records = dao.getAll() val records = dao.getAll()
.asSequence() .asSequence()
@@ -286,6 +293,7 @@ private fun QsoEntity.toDomain() = QsoRecord(
propagationMode = propagationMode, propagationMode = propagationMode,
lotwConfirmed = lotwConfirmed, lotwConfirmed = lotwConfirmed,
lotwUploaded = lotwUploaded, lotwUploaded = lotwUploaded,
wavelogUploaded = wavelogUploaded,
lotwReceived = lotwReceived, lotwReceived = lotwReceived,
lotwQslDate = lotwQslDate, lotwQslDate = lotwQslDate,
vuccGrids = vuccGrids.split(',').filter(String::isNotBlank), vuccGrids = vuccGrids.split(',').filter(String::isNotBlank),
@@ -323,6 +331,7 @@ private fun QsoRecord.toEntity() = QsoEntity(
propagationMode = propagationMode.ifBlank { if (satelliteName.isNotBlank()) "SAT" else "" }, propagationMode = propagationMode.ifBlank { if (satelliteName.isNotBlank()) "SAT" else "" },
lotwConfirmed = lotwConfirmed, lotwConfirmed = lotwConfirmed,
lotwUploaded = lotwUploaded, lotwUploaded = lotwUploaded,
wavelogUploaded = wavelogUploaded,
lotwReceived = lotwReceived, lotwReceived = lotwReceived,
lotwQslDate = lotwQslDate, lotwQslDate = lotwQslDate,
vuccGrids = vuccGrids.joinToString(","), vuccGrids = vuccGrids.joinToString(","),
@@ -295,6 +295,27 @@ class QsoRepositoryTest {
assertEquals(1, dao.getAll().size) 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. */ /** A record as the log page creates it: tracker name, repeater pair, uplink band. */
private fun loggedInApp(satelliteName: String, lotwUploaded: Boolean) = QsoRecord( private fun loggedInApp(satelliteName: String, lotwUploaded: Boolean) = QsoRecord(
startUtcMillis = qsoStart, startUtcMillis = qsoStart,
@@ -355,6 +376,7 @@ class QsoRepositoryTest {
propagationMode = propagationMode, propagationMode = propagationMode,
lotwConfirmed = lotwConfirmed, lotwConfirmed = lotwConfirmed,
lotwUploaded = lotwUploaded, lotwUploaded = lotwUploaded,
wavelogUploaded = wavelogUploaded,
lotwReceived = lotwReceived, lotwReceived = lotwReceived,
lotwQslDate = lotwQslDate, lotwQslDate = lotwQslDate,
vuccGrids = vuccGrids.joinToString(","), vuccGrids = vuccGrids.joinToString(","),
@@ -393,5 +415,9 @@ class QsoRepositoryTest {
override suspend fun markUploaded(ids: List<Long>) { override suspend fun markUploaded(ids: List<Long>) {
ids.forEach { id -> rows[id]?.let { rows[id] = it.copy(lotwUploaded = true) } } 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. * logbook row can show which grids the QSO went out under.
*/ */
suspend fun markUploaded(ids: List<Long>, grids: List<String> = emptyList()) 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 exportAdi(ids: Set<Long>? = null, includeIncomplete: Boolean = false): String
suspend fun importAdi(content: String): AdifImportResult suspend fun importAdi(content: String): AdifImportResult
suspend fun mergeConfirmed(records: List<QsoRecord>): AdifImportResult suspend fun mergeConfirmed(records: List<QsoRecord>): AdifImportResult
@@ -40,6 +40,8 @@ data class QsoRecord(
val propagationMode: String = if (satelliteName.isNotBlank()) "SAT" else "", val propagationMode: String = if (satelliteName.isNotBlank()) "SAT" else "",
val lotwConfirmed: Boolean = false, val lotwConfirmed: Boolean = false,
val lotwUploaded: 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 lotwReceived: Boolean = false,
val lotwQslDate: String = "", val lotwQslDate: String = "",
val vuccGrids: List<String> = emptyList(), val vuccGrids: List<String> = emptyList(),