From 4585332b1a40e55dee80ec5bb7bf291a98dd1e43 Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Thu, 30 Jul 2026 14:10:49 +0200 Subject: [PATCH] Fixed manual OMM (.csv) data import, tweaked messaging --- .../core/data/repository/DatabaseRepo.kt | 40 ++- .../{ => framework}/Ft817CatProtocolTest.kt | 39 +-- .../core/data/repository/DatabaseRepoTest.kt | 234 ++++++++++++++++++ .../core/domain/repository/IDatabaseRepo.kt | 4 +- .../src/main/res/values/strings.xml | 2 + .../feature/settings/SettingsDialog.kt | 2 +- .../feature/settings/SettingsScreen.kt | 9 +- .../feature/settings/SettingsState.kt | 9 +- .../feature/settings/SettingsViewModel.kt | 34 ++- gradle/libs.versions.toml | 8 +- 10 files changed, 333 insertions(+), 48 deletions(-) rename core/data/src/test/java/com/rtbishop/look4sat/core/data/{ => framework}/Ft817CatProtocolTest.kt (75%) create mode 100644 core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt index 957d5b1d..8decc7a6 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt @@ -42,21 +42,27 @@ class DatabaseRepo( private val customSourceType = "Other" - override suspend fun updateTLEFromFile(uri: String) = withContext(dispatcher) { + override suspend fun updateTLEFromFile(uri: String): Int = withContext(dispatcher) { + var importedCount = 0 remoteSource.getFileStream(uri)?.let { stream -> - val entries = dataParser.parseTLEStream(unwrapIfZipped(uri, stream)) + val entries = parseSatelliteStream(uri, unwrapIfZipped(uri, stream)) localSource.insertEntries(entries) settingsRepo.setSatelliteTypeIds(customSourceType, entries.map { it.catnum }) + importedCount = entries.size } setUpdateSuccessful(System.currentTimeMillis()) + importedCount } - override suspend fun updateTransceiversFromFile(uri: String) = withContext(dispatcher) { + override suspend fun updateTransceiversFromFile(uri: String): Int = withContext(dispatcher) { + var importedCount = 0 remoteSource.getFileStream(uri)?.let { stream -> val transceivers = dataParser.parseJSONStream(unwrapIfZipped(uri, stream)) localSource.insertRadios(transceivers) + importedCount = transceivers.size } setUpdateSuccessful(System.currentTimeMillis()) + importedCount } override suspend fun updateFromRemote() = withContext(dispatcher) { @@ -94,9 +100,31 @@ class DatabaseRepo( setUpdateSuccessful(0L) } - private suspend fun parseSatelliteStream(url: String, stream: InputStream): List = when { - url.contains("FORMAT=csv", ignoreCase = true) -> dataParser.parseCSVStream(stream) - else -> dataParser.parseTLEStream(stream) + private suspend fun parseSatelliteStream(url: String, stream: InputStream): List { + val bufferedStream = stream.buffered() + return when { + hasCsvHint(url) || looksLikeCsv(bufferedStream) -> dataParser.parseCSVStream(bufferedStream) + else -> dataParser.parseTLEStream(bufferedStream) + } + } + + private fun hasCsvHint(url: String): Boolean { + return url.contains("FORMAT=csv", ignoreCase = true) || + url.endsWith(".csv", ignoreCase = true) || + url.endsWith(".csv.zip", ignoreCase = true) + } + + private fun looksLikeCsv(stream: InputStream): Boolean { + if (!stream.markSupported()) return false + stream.mark(4096) + val preview = ByteArray(4096) + val length = stream.read(preview) + stream.reset() + if (length <= 0) return false + val line = preview.decodeToString(0, length).lineSequence().firstOrNull()?.trim().orEmpty() + return line.contains("OBJECT_NAME", ignoreCase = true) || + line.contains("NORAD_CAT_ID", ignoreCase = true) || + line.count { it == ',' } >= 4 } private suspend fun setUpdateSuccessful(timestamp: Long) { diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/Ft817CatProtocolTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/framework/Ft817CatProtocolTest.kt similarity index 75% rename from core/data/src/test/java/com/rtbishop/look4sat/core/data/Ft817CatProtocolTest.kt rename to core/data/src/test/java/com/rtbishop/look4sat/core/data/framework/Ft817CatProtocolTest.kt index dd263ce1..841954eb 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/Ft817CatProtocolTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/framework/Ft817CatProtocolTest.kt @@ -1,30 +1,29 @@ -package com.rtbishop.look4sat.core.data +package com.rtbishop.look4sat.core.data.framework -import com.rtbishop.look4sat.core.data.framework.Ft817CatProtocol +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull class Ft817CatProtocolTest { @Test fun encodeFrequencyBcd_145500000() { val bcd = Ft817CatProtocol.encodeFrequencyBcd(145500000L) - assertContentEquals(byteArrayOf(0x14, 0x55, 0x00, 0x00), bcd) + assertArrayEquals(byteArrayOf(0x14, 0x55, 0x00, 0x00), bcd) } @Test fun encodeFrequencyBcd_435100000() { val bcd = Ft817CatProtocol.encodeFrequencyBcd(435100000L) - assertContentEquals(byteArrayOf(0x43, 0x51, 0x00, 0x00), bcd) + assertArrayEquals(byteArrayOf(0x43, 0x51, 0x00, 0x00), bcd) } @Test fun encodeFrequencyBcd_7074000() { val bcd = Ft817CatProtocol.encodeFrequencyBcd(7074000L) - assertContentEquals(byteArrayOf(0x00, 0x70, 0x74, 0x00), bcd) + assertArrayEquals(byteArrayOf(0x00, 0x70, 0x74, 0x00), bcd) } @Test @@ -42,21 +41,21 @@ class Ft817CatProtocolTest { val cmd = Ft817CatProtocol.buildSetFreqCommand(145500000L) assertEquals(5, cmd.size) assertEquals(0x01.toByte(), cmd[4]) - assertContentEquals(byteArrayOf(0x14, 0x55, 0x00, 0x00, 0x01), cmd) + assertArrayEquals(byteArrayOf(0x14, 0x55, 0x00, 0x00, 0x01), cmd) } @Test fun buildSetModeCommand_usb() { val cmd = Ft817CatProtocol.buildSetModeCommand("USB") assertNotNull(cmd) - assertContentEquals(byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x07), cmd) + assertArrayEquals(byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x07), cmd) } @Test fun buildSetModeCommand_fm() { val cmd = Ft817CatProtocol.buildSetModeCommand("FM") assertNotNull(cmd) - assertContentEquals(byteArrayOf(0x08, 0x00, 0x00, 0x00, 0x07), cmd) + assertArrayEquals(byteArrayOf(0x08, 0x00, 0x00, 0x00, 0x07), cmd) } @Test @@ -67,19 +66,19 @@ class Ft817CatProtocolTest { @Test fun encodeCtcssTone_67_0() { val bcd = Ft817CatProtocol.encodeCtcssToneBcd(67.0) - assertContentEquals(byteArrayOf(0x06, 0x70), bcd) + assertArrayEquals(byteArrayOf(0x06, 0x70), bcd) } @Test fun encodeCtcssTone_74_4() { val bcd = Ft817CatProtocol.encodeCtcssToneBcd(74.4) - assertContentEquals(byteArrayOf(0x07, 0x44), bcd) + assertArrayEquals(byteArrayOf(0x07, 0x44), bcd) } @Test fun encodeCtcssTone_141_3() { val bcd = Ft817CatProtocol.encodeCtcssToneBcd(141.3) - assertContentEquals(byteArrayOf(0x14, 0x13), bcd) + assertArrayEquals(byteArrayOf(0x14, 0x13), bcd) } @Test @@ -87,13 +86,13 @@ class Ft817CatProtocolTest { val cmd = Ft817CatProtocol.buildSetCtcssToneCommand(67.0) assertEquals(5, cmd.size) assertEquals(0x0B.toByte(), cmd[4]) - assertContentEquals(byteArrayOf(0x06, 0x70, 0x00, 0x00, 0x0B), cmd) + assertArrayEquals(byteArrayOf(0x06, 0x70, 0x00, 0x00, 0x0B), cmd) } @Test fun buildCtcssModeCommand_enable() { val cmd = Ft817CatProtocol.buildCtcssModeCommand(true) - assertContentEquals(byteArrayOf(0x2A, 0x00, 0x00, 0x00, 0x0A), cmd) + assertArrayEquals(byteArrayOf(0x2A, 0x00, 0x00, 0x00, 0x0A), cmd) } @Test @@ -108,6 +107,7 @@ class Ft817CatProtocolTest { val response = byteArrayOf(0x14, 0x55, 0x00, 0x00, 0x01) val result = Ft817CatProtocol.parseReadResponse(response) assertNotNull(result) + result ?: return assertEquals(145500000L, result.first) assertEquals("USB", result.second) } @@ -117,6 +117,7 @@ class Ft817CatProtocolTest { val response = byteArrayOf(0x14, 0x60, 0x00, 0x00, 0x08) val result = Ft817CatProtocol.parseReadResponse(response) assertNotNull(result) + result ?: return assertEquals(146000000L, result.first) assertEquals("FM", result.second) } @@ -135,7 +136,7 @@ class Ft817CatProtocolTest { @Test fun buildPttCommands() { val on = Ft817CatProtocol.buildPttOnCommand() - assertContentEquals(byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x08), on) + assertArrayEquals(byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x08), on) val off = Ft817CatProtocol.buildPttOffCommand() assertEquals(0x88.toByte(), off[4]) @@ -144,6 +145,6 @@ class Ft817CatProtocolTest { @Test fun buildReadCommand() { val cmd = Ft817CatProtocol.buildReadFreqModeCommand() - assertContentEquals(byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x03), cmd) + assertArrayEquals(byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x03), cmd) } } diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt new file mode 100644 index 00000000..a14ede56 --- /dev/null +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt @@ -0,0 +1,234 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.rtbishop.look4sat.core.data.repository + +import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings +import com.rtbishop.look4sat.core.domain.model.DatabaseState +import com.rtbishop.look4sat.core.domain.model.OtherSettings +import com.rtbishop.look4sat.core.domain.model.PassesSettings +import com.rtbishop.look4sat.core.domain.model.RCSettings +import com.rtbishop.look4sat.core.domain.model.RadioControlSettings +import com.rtbishop.look4sat.core.domain.model.SatItem +import com.rtbishop.look4sat.core.domain.model.SatRadio +import com.rtbishop.look4sat.core.domain.predict.GeoPos +import com.rtbishop.look4sat.core.domain.predict.OrbitalData +import com.rtbishop.look4sat.core.domain.predict.OrbitalObject +import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo +import com.rtbishop.look4sat.core.domain.source.ILocalSource +import com.rtbishop.look4sat.core.domain.source.IRemoteSource +import com.rtbishop.look4sat.core.domain.utility.DataParser +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.InputStream + +@OptIn(ExperimentalCoroutinesApi::class) +class DatabaseRepoTest { + + private val dispatcher = StandardTestDispatcher() + private val dataParser = DataParser(dispatcher) + + @Test + fun `manual satellite import parses csv stream from content uri`() = runTest(dispatcher) { + val uri = "content://look4sat/import/satellites" + val localSource = FakeLocalSource() + val remoteSource = FakeRemoteSource().apply { + fileStreams[uri] = { validCsvStream() } + } + val settingsRepo = FakeSettingsRepo() + val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo) + + repository.updateTLEFromFile(uri) + + assertEquals(1, localSource.insertedEntries.size) + assertEquals(25544, localSource.insertedEntries.first().catnum) + assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["Other"]) + assertTrue(settingsRepo.databaseState.value.numberOfSatellites > 0) + } + + @Test + fun `manual satellite import keeps tle support`() = runTest(dispatcher) { + val uri = "content://look4sat/import/legacy" + val localSource = FakeLocalSource() + val remoteSource = FakeRemoteSource().apply { + fileStreams[uri] = { validTleStream() } + } + val settingsRepo = FakeSettingsRepo() + val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo) + + repository.updateTLEFromFile(uri) + + assertEquals(1, localSource.insertedEntries.size) + assertEquals(25544, localSource.insertedEntries.first().catnum) + } + + @Test + fun `custom data source imports omm csv from web`() = runTest(dispatcher) { + val customCsvUrl = "https://example.com/custom-omm.csv" + val localSource = FakeLocalSource() + val remoteSource = FakeRemoteSource().apply { + networkStreams[customCsvUrl] = { validCsvStream() } + } + val settingsRepo = FakeSettingsRepo( + dataSources = DataSourcesSettings( + useCustomTLE = true, + useCustomTransceivers = false, + tleUrl = customCsvUrl, + transceiversUrl = "" + ) + ) + val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo) + + repository.updateFromRemote() + + assertTrue(localSource.insertedEntries.any { it.catnum == 25544 }) + assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["Other"]) + } + + private fun validCsvStream(): InputStream = """ + OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT + ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0 + """.trimIndent().byteInputStream() + + private fun validTleStream(): InputStream = """ + ISS (ZARYA) + 1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990 + 2 25544 51.6447 309.4881 0004694 203.6966 299.8876 15.48582035312205 + """.trimIndent().byteInputStream() +} + +private class FakeRemoteSource : IRemoteSource { + val fileStreams: MutableMap InputStream> = mutableMapOf() + val networkStreams: MutableMap InputStream> = mutableMapOf() + + override suspend fun getFileStream(uri: String): InputStream? = fileStreams[uri]?.invoke() + + override suspend fun getNetworkStream(url: String): InputStream? = networkStreams[url]?.invoke() +} + +private class FakeLocalSource : ILocalSource { + val insertedEntries = mutableListOf() + private val insertedRadios = mutableListOf() + + override suspend fun getEntriesTotal(): Int = insertedEntries.size + + override suspend fun getEntriesList(): List = emptyList() + + override suspend fun getEntriesWithIds(ids: List): List = emptyList() + + override suspend fun insertEntries(entries: List) { + insertedEntries += entries + } + + override suspend fun deleteEntries() { + insertedEntries.clear() + } + + override suspend fun getIdsWithModes(modes: List): List = emptyList() + + override suspend fun getRadiosTotal(): Int = insertedRadios.size + + override suspend fun getRadiosWithId(id: Int): List = emptyList() + + override suspend fun insertRadios(radios: List) { + insertedRadios += radios + } + + override suspend fun deleteRadios() { + insertedRadios.clear() + } +} + +private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSourcesSettings()) : ISettingsRepo { + + override val appVersionName: String = "test" + + override val selectedIds: StateFlow> = MutableStateFlow(emptyList()) + + override val selectedTypes: StateFlow> = MutableStateFlow(emptyList()) + + override val passesSettings: StateFlow = MutableStateFlow( + PassesSettings(hoursAhead = 24, minElevation = 0.0, selectedModes = emptyList()) + ) + + override val stationPosition: StateFlow = MutableStateFlow(GeoPos(0.0, 0.0)) + + override val databaseState: MutableStateFlow = MutableStateFlow(DatabaseState(0, 0, 0L)) + + override val rcSettings: StateFlow = MutableStateFlow( + RCSettings(false, "", "", "", false, "", "", "", false, "", "", "", false, "", "") + ) + + override val otherSettings: StateFlow = MutableStateFlow( + OtherSettings(false, false, false, false, false, false, false, false) + ) + + override val dataSourcesSettings: MutableStateFlow = MutableStateFlow(dataSources) + + override val radioControlSettings: StateFlow = MutableStateFlow( + RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600) + ) + + val satelliteTypeIdsByType = mutableMapOf>() + + override fun setSelectedIds(ids: List) = Unit + + override fun setSelectedTypes(types: List) = Unit + + override fun setPassesSettings(settings: PassesSettings) = Unit + + override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean = true + + override fun setStationPosition(): Boolean = true + + override fun setStationPosition(locator: String): Boolean = true + + override fun getSatelliteTypesIds(types: List): List = emptyList() + + override fun setSatelliteTypeIds(type: String, ids: List) { + satelliteTypeIdsByType[type] = ids + } + + override fun updateDatabaseState(state: DatabaseState) { + databaseState.value = state + } + + override fun updateRCSettings(settings: RCSettings) = Unit + + override fun updateOtherSettings(transform: (OtherSettings) -> OtherSettings) = Unit + + override fun updateDataSourcesSettings(settings: DataSourcesSettings) { + dataSourcesSettings.value = settings + } + + override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit +} + +private fun defaultDataSourcesSettings(): DataSourcesSettings { + return DataSourcesSettings( + useCustomTLE = false, + useCustomTransceivers = false, + tleUrl = "", + transceiversUrl = "" + ) +} diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IDatabaseRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IDatabaseRepo.kt index c496f566..049d121e 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IDatabaseRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IDatabaseRepo.kt @@ -18,8 +18,8 @@ package com.rtbishop.look4sat.core.domain.repository interface IDatabaseRepo { - suspend fun updateTLEFromFile(uri: String) - suspend fun updateTransceiversFromFile(uri: String) + suspend fun updateTLEFromFile(uri: String): Int + suspend fun updateTransceiversFromFile(uri: String): Int suspend fun updateFromRemote() suspend fun clearAllData() } diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 52ffb57a..530545e5 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -138,6 +138,8 @@ Clear Data was cleared successfully Update completed successfully + No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file. + No transceivers imported. Select a valid SatNOGS (.json) file. Custom data sources Custom TLE URL diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt index 9b799dfd..fbeccea3 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt @@ -161,7 +161,7 @@ fun DataSourcesDialog( onImportTle() onDismiss() }, - text = "TLE (3LE)\nR4UAB (.txt)", + text = "TLE/3LE (.txt)\nOMM (.csv)", modifier = Modifier.weight(1f) ) CardButton( diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index 4d96e05a..9d7a1263 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -357,7 +357,7 @@ private fun LocationCard( @Composable private fun DataCardPreview() = MainTheme { val settings = DataSettings(true, 5000, 2500, 0L) - DataCard(settings = settings, {}, {}, {}) + DataCard(settings = settings, updateFromWeb = {}, clearAllData = {}, showDataSourcesDialog = {}) } @Composable @@ -672,12 +672,15 @@ private fun rememberSettingsPermissions( else sendAction(SettingsAction.ShowToast(locationError)) } + val satellitesImportError = stringResource(R.string.prefs_data_import_satellites_error) + val transceiversImportError = stringResource(R.string.prefs_data_import_transceivers_error) + val tleRequest = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let { sendAction(SettingsAction.UpdateTLEFromFile(it.toString())) } + uri?.let { sendAction(SettingsAction.UpdateTLEFromFile(it.toString(), satellitesImportError)) } } val transceiversRequest = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let { sendAction(SettingsAction.UpdateTransceiversFromFile(it.toString())) } + uri?.let { sendAction(SettingsAction.UpdateTransceiversFromFile(it.toString(), transceiversImportError)) } } val bluetoothError = stringResource(R.string.prefs_bt_perm_error) diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt index 8943ab2d..928cf60b 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt @@ -28,7 +28,10 @@ data class PositionSettings( ) data class DataSettings( - val isUpdating: Boolean, val entriesTotal: Int, val radiosTotal: Int, val timestamp: Long + val isUpdating: Boolean, + val entriesTotal: Int, + val radiosTotal: Int, + val timestamp: Long ) data class SettingsState( @@ -50,8 +53,8 @@ sealed interface SettingsAction { // Data data object UpdateFromWeb : SettingsAction - data class UpdateTLEFromFile(val uri: String) : SettingsAction - data class UpdateTransceiversFromFile(val uri: String) : SettingsAction + data class UpdateTLEFromFile(val uri: String, val invalidFileMessage: String) : SettingsAction + data class UpdateTransceiversFromFile(val uri: String, val invalidFileMessage: String) : SettingsAction data object ClearAllData : SettingsAction // Toggles diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt index 6a685155..f43e91f6 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt @@ -65,11 +65,11 @@ class SettingsViewModel( settingsRepo.databaseState.collect { state -> _uiState.update { it.copy( - dataSettings = DataSettings( - false, - state.numberOfSatellites, - state.numberOfRadios, - state.updateTimestamp + dataSettings = it.dataSettings.copy( + isUpdating = false, + entriesTotal = state.numberOfSatellites, + radiosTotal = state.numberOfRadios, + timestamp = state.updateTimestamp ) ) } @@ -107,11 +107,11 @@ class SettingsViewModel( SettingsAction.DismissPosMessages -> dismissPosMessage() // Data SettingsAction.UpdateFromWeb -> runDataUpdate { databaseRepo.updateFromRemote() } - is SettingsAction.UpdateTLEFromFile -> runDataUpdate { databaseRepo.updateTLEFromFile(action.uri) } - is SettingsAction.UpdateTransceiversFromFile -> runDataUpdate { - databaseRepo.updateTransceiversFromFile( - action.uri - ) + is SettingsAction.UpdateTLEFromFile -> runManualImport(action.invalidFileMessage) { + databaseRepo.updateTLEFromFile(action.uri) + } + is SettingsAction.UpdateTransceiversFromFile -> runManualImport(action.invalidFileMessage) { + databaseRepo.updateTransceiversFromFile(action.uri) } SettingsAction.ClearAllData -> viewModelScope.launch { databaseRepo.clearAllData() } // Toggles @@ -184,9 +184,23 @@ class SettingsViewModel( } } + private fun runManualImport(importErrorMessage: String, block: suspend () -> Int) { + viewModelScope.launch { + try { + _uiState.update { it.copy(dataSettings = it.dataSettings.copy(isUpdating = true)) } + if (block() == 0) showToast(importErrorMessage) + } catch (exception: Exception) { + _uiState.update { it.copy(dataSettings = it.dataSettings.copy(isUpdating = false)) } + println(exception) + } + } + } + + // endregion companion object { + fun factory(container: IMainContainer) = viewModelFactory { initializer { SettingsViewModel( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b49a71a7..6f817fc0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,20 +12,20 @@ jdkVersion = "21" #noinspection UnusedVersionCatalogEntry packageName = "com.rtbishop.look4sat" -android-gradle-plugin = "9.2.1" +android-gradle-plugin = "9.3.1" androidx-core-ktx = "1.19.0" androidx-core-splashscreen = "1.2.0" androidx-room = "2.8.4" -compose-bom = "2026.06.00" +compose-bom = "2026.06.01" compose-activity = "1.13.0" compose-lifecycle = "2.11.0" -compose-navigation3 = "1.1.3" +compose-navigation3 = "1.1.5" google-ksp = "2.3.8" -kotlin = "2.4.0" +kotlin = "2.4.10" kotlin-coroutines = "1.11.0" kotlin-serialization = "1.11.0"