mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Fixed manual OMM (.csv) data import, tweaked messaging
This commit is contained in:
1 parent
6d2b0ced49
commit
4585332b1a
10 files changed
+333
-48
No files matched your search
@@ -42,21 +42,27 @@ class DatabaseRepo(
|
|||||||
|
|
||||||
private val customSourceType = "Other"
|
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 ->
|
remoteSource.getFileStream(uri)?.let { stream ->
|
||||||
val entries = dataParser.parseTLEStream(unwrapIfZipped(uri, stream))
|
val entries = parseSatelliteStream(uri, unwrapIfZipped(uri, stream))
|
||||||
localSource.insertEntries(entries)
|
localSource.insertEntries(entries)
|
||||||
settingsRepo.setSatelliteTypeIds(customSourceType, entries.map { it.catnum })
|
settingsRepo.setSatelliteTypeIds(customSourceType, entries.map { it.catnum })
|
||||||
|
importedCount = entries.size
|
||||||
}
|
}
|
||||||
setUpdateSuccessful(System.currentTimeMillis())
|
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 ->
|
remoteSource.getFileStream(uri)?.let { stream ->
|
||||||
val transceivers = dataParser.parseJSONStream(unwrapIfZipped(uri, stream))
|
val transceivers = dataParser.parseJSONStream(unwrapIfZipped(uri, stream))
|
||||||
localSource.insertRadios(transceivers)
|
localSource.insertRadios(transceivers)
|
||||||
|
importedCount = transceivers.size
|
||||||
}
|
}
|
||||||
setUpdateSuccessful(System.currentTimeMillis())
|
setUpdateSuccessful(System.currentTimeMillis())
|
||||||
|
importedCount
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateFromRemote() = withContext(dispatcher) {
|
override suspend fun updateFromRemote() = withContext(dispatcher) {
|
||||||
@@ -94,9 +100,31 @@ class DatabaseRepo(
|
|||||||
setUpdateSuccessful(0L)
|
setUpdateSuccessful(0L)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun parseSatelliteStream(url: String, stream: InputStream): List<OrbitalData> = when {
|
private suspend fun parseSatelliteStream(url: String, stream: InputStream): List<OrbitalData> {
|
||||||
url.contains("FORMAT=csv", ignoreCase = true) -> dataParser.parseCSVStream(stream)
|
val bufferedStream = stream.buffered()
|
||||||
else -> dataParser.parseTLEStream(stream)
|
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) {
|
private suspend fun setUpdateSuccessful(timestamp: Long) {
|
||||||
|
|||||||
+20
-19
@@ -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 org.junit.Test
|
||||||
import kotlin.test.assertContentEquals
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
import kotlin.test.assertNull
|
|
||||||
|
|
||||||
class Ft817CatProtocolTest {
|
class Ft817CatProtocolTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun encodeFrequencyBcd_145500000() {
|
fun encodeFrequencyBcd_145500000() {
|
||||||
val bcd = Ft817CatProtocol.encodeFrequencyBcd(145500000L)
|
val bcd = Ft817CatProtocol.encodeFrequencyBcd(145500000L)
|
||||||
assertContentEquals(byteArrayOf(0x14, 0x55, 0x00, 0x00), bcd)
|
assertArrayEquals(byteArrayOf(0x14, 0x55, 0x00, 0x00), bcd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun encodeFrequencyBcd_435100000() {
|
fun encodeFrequencyBcd_435100000() {
|
||||||
val bcd = Ft817CatProtocol.encodeFrequencyBcd(435100000L)
|
val bcd = Ft817CatProtocol.encodeFrequencyBcd(435100000L)
|
||||||
assertContentEquals(byteArrayOf(0x43, 0x51, 0x00, 0x00), bcd)
|
assertArrayEquals(byteArrayOf(0x43, 0x51, 0x00, 0x00), bcd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun encodeFrequencyBcd_7074000() {
|
fun encodeFrequencyBcd_7074000() {
|
||||||
val bcd = Ft817CatProtocol.encodeFrequencyBcd(7074000L)
|
val bcd = Ft817CatProtocol.encodeFrequencyBcd(7074000L)
|
||||||
assertContentEquals(byteArrayOf(0x00, 0x70, 0x74, 0x00), bcd)
|
assertArrayEquals(byteArrayOf(0x00, 0x70, 0x74, 0x00), bcd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -42,21 +41,21 @@ class Ft817CatProtocolTest {
|
|||||||
val cmd = Ft817CatProtocol.buildSetFreqCommand(145500000L)
|
val cmd = Ft817CatProtocol.buildSetFreqCommand(145500000L)
|
||||||
assertEquals(5, cmd.size)
|
assertEquals(5, cmd.size)
|
||||||
assertEquals(0x01.toByte(), cmd[4])
|
assertEquals(0x01.toByte(), cmd[4])
|
||||||
assertContentEquals(byteArrayOf(0x14, 0x55, 0x00, 0x00, 0x01), cmd)
|
assertArrayEquals(byteArrayOf(0x14, 0x55, 0x00, 0x00, 0x01), cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun buildSetModeCommand_usb() {
|
fun buildSetModeCommand_usb() {
|
||||||
val cmd = Ft817CatProtocol.buildSetModeCommand("USB")
|
val cmd = Ft817CatProtocol.buildSetModeCommand("USB")
|
||||||
assertNotNull(cmd)
|
assertNotNull(cmd)
|
||||||
assertContentEquals(byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x07), cmd)
|
assertArrayEquals(byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x07), cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun buildSetModeCommand_fm() {
|
fun buildSetModeCommand_fm() {
|
||||||
val cmd = Ft817CatProtocol.buildSetModeCommand("FM")
|
val cmd = Ft817CatProtocol.buildSetModeCommand("FM")
|
||||||
assertNotNull(cmd)
|
assertNotNull(cmd)
|
||||||
assertContentEquals(byteArrayOf(0x08, 0x00, 0x00, 0x00, 0x07), cmd)
|
assertArrayEquals(byteArrayOf(0x08, 0x00, 0x00, 0x00, 0x07), cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -67,19 +66,19 @@ class Ft817CatProtocolTest {
|
|||||||
@Test
|
@Test
|
||||||
fun encodeCtcssTone_67_0() {
|
fun encodeCtcssTone_67_0() {
|
||||||
val bcd = Ft817CatProtocol.encodeCtcssToneBcd(67.0)
|
val bcd = Ft817CatProtocol.encodeCtcssToneBcd(67.0)
|
||||||
assertContentEquals(byteArrayOf(0x06, 0x70), bcd)
|
assertArrayEquals(byteArrayOf(0x06, 0x70), bcd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun encodeCtcssTone_74_4() {
|
fun encodeCtcssTone_74_4() {
|
||||||
val bcd = Ft817CatProtocol.encodeCtcssToneBcd(74.4)
|
val bcd = Ft817CatProtocol.encodeCtcssToneBcd(74.4)
|
||||||
assertContentEquals(byteArrayOf(0x07, 0x44), bcd)
|
assertArrayEquals(byteArrayOf(0x07, 0x44), bcd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun encodeCtcssTone_141_3() {
|
fun encodeCtcssTone_141_3() {
|
||||||
val bcd = Ft817CatProtocol.encodeCtcssToneBcd(141.3)
|
val bcd = Ft817CatProtocol.encodeCtcssToneBcd(141.3)
|
||||||
assertContentEquals(byteArrayOf(0x14, 0x13), bcd)
|
assertArrayEquals(byteArrayOf(0x14, 0x13), bcd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -87,13 +86,13 @@ class Ft817CatProtocolTest {
|
|||||||
val cmd = Ft817CatProtocol.buildSetCtcssToneCommand(67.0)
|
val cmd = Ft817CatProtocol.buildSetCtcssToneCommand(67.0)
|
||||||
assertEquals(5, cmd.size)
|
assertEquals(5, cmd.size)
|
||||||
assertEquals(0x0B.toByte(), cmd[4])
|
assertEquals(0x0B.toByte(), cmd[4])
|
||||||
assertContentEquals(byteArrayOf(0x06, 0x70, 0x00, 0x00, 0x0B), cmd)
|
assertArrayEquals(byteArrayOf(0x06, 0x70, 0x00, 0x00, 0x0B), cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun buildCtcssModeCommand_enable() {
|
fun buildCtcssModeCommand_enable() {
|
||||||
val cmd = Ft817CatProtocol.buildCtcssModeCommand(true)
|
val cmd = Ft817CatProtocol.buildCtcssModeCommand(true)
|
||||||
assertContentEquals(byteArrayOf(0x2A, 0x00, 0x00, 0x00, 0x0A), cmd)
|
assertArrayEquals(byteArrayOf(0x2A, 0x00, 0x00, 0x00, 0x0A), cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -108,6 +107,7 @@ class Ft817CatProtocolTest {
|
|||||||
val response = byteArrayOf(0x14, 0x55, 0x00, 0x00, 0x01)
|
val response = byteArrayOf(0x14, 0x55, 0x00, 0x00, 0x01)
|
||||||
val result = Ft817CatProtocol.parseReadResponse(response)
|
val result = Ft817CatProtocol.parseReadResponse(response)
|
||||||
assertNotNull(result)
|
assertNotNull(result)
|
||||||
|
result ?: return
|
||||||
assertEquals(145500000L, result.first)
|
assertEquals(145500000L, result.first)
|
||||||
assertEquals("USB", result.second)
|
assertEquals("USB", result.second)
|
||||||
}
|
}
|
||||||
@@ -117,6 +117,7 @@ class Ft817CatProtocolTest {
|
|||||||
val response = byteArrayOf(0x14, 0x60, 0x00, 0x00, 0x08)
|
val response = byteArrayOf(0x14, 0x60, 0x00, 0x00, 0x08)
|
||||||
val result = Ft817CatProtocol.parseReadResponse(response)
|
val result = Ft817CatProtocol.parseReadResponse(response)
|
||||||
assertNotNull(result)
|
assertNotNull(result)
|
||||||
|
result ?: return
|
||||||
assertEquals(146000000L, result.first)
|
assertEquals(146000000L, result.first)
|
||||||
assertEquals("FM", result.second)
|
assertEquals("FM", result.second)
|
||||||
}
|
}
|
||||||
@@ -135,7 +136,7 @@ class Ft817CatProtocolTest {
|
|||||||
@Test
|
@Test
|
||||||
fun buildPttCommands() {
|
fun buildPttCommands() {
|
||||||
val on = Ft817CatProtocol.buildPttOnCommand()
|
val on = Ft817CatProtocol.buildPttOnCommand()
|
||||||
assertContentEquals(byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x08), on)
|
assertArrayEquals(byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x08), on)
|
||||||
|
|
||||||
val off = Ft817CatProtocol.buildPttOffCommand()
|
val off = Ft817CatProtocol.buildPttOffCommand()
|
||||||
assertEquals(0x88.toByte(), off[4])
|
assertEquals(0x88.toByte(), off[4])
|
||||||
@@ -144,6 +145,6 @@ class Ft817CatProtocolTest {
|
|||||||
@Test
|
@Test
|
||||||
fun buildReadCommand() {
|
fun buildReadCommand() {
|
||||||
val cmd = Ft817CatProtocol.buildReadFreqModeCommand()
|
val cmd = Ft817CatProtocol.buildReadFreqModeCommand()
|
||||||
assertContentEquals(byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x03), cmd)
|
assertArrayEquals(byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x03), cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+234
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<String, () -> InputStream> = mutableMapOf()
|
||||||
|
val networkStreams: MutableMap<String, () -> 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<OrbitalData>()
|
||||||
|
private val insertedRadios = mutableListOf<SatRadio>()
|
||||||
|
|
||||||
|
override suspend fun getEntriesTotal(): Int = insertedEntries.size
|
||||||
|
|
||||||
|
override suspend fun getEntriesList(): List<SatItem> = emptyList()
|
||||||
|
|
||||||
|
override suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject> = emptyList()
|
||||||
|
|
||||||
|
override suspend fun insertEntries(entries: List<OrbitalData>) {
|
||||||
|
insertedEntries += entries
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteEntries() {
|
||||||
|
insertedEntries.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getIdsWithModes(modes: List<String>): List<Int> = emptyList()
|
||||||
|
|
||||||
|
override suspend fun getRadiosTotal(): Int = insertedRadios.size
|
||||||
|
|
||||||
|
override suspend fun getRadiosWithId(id: Int): List<SatRadio> = emptyList()
|
||||||
|
|
||||||
|
override suspend fun insertRadios(radios: List<SatRadio>) {
|
||||||
|
insertedRadios += radios
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteRadios() {
|
||||||
|
insertedRadios.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSourcesSettings()) : ISettingsRepo {
|
||||||
|
|
||||||
|
override val appVersionName: String = "test"
|
||||||
|
|
||||||
|
override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
|
override val selectedTypes: StateFlow<List<String>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
|
override val passesSettings: StateFlow<PassesSettings> = MutableStateFlow(
|
||||||
|
PassesSettings(hoursAhead = 24, minElevation = 0.0, selectedModes = emptyList())
|
||||||
|
)
|
||||||
|
|
||||||
|
override val stationPosition: StateFlow<GeoPos> = MutableStateFlow(GeoPos(0.0, 0.0))
|
||||||
|
|
||||||
|
override val databaseState: MutableStateFlow<DatabaseState> = MutableStateFlow(DatabaseState(0, 0, 0L))
|
||||||
|
|
||||||
|
override val rcSettings: StateFlow<RCSettings> = MutableStateFlow(
|
||||||
|
RCSettings(false, "", "", "", false, "", "", "", false, "", "", "", false, "", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
override val otherSettings: StateFlow<OtherSettings> = MutableStateFlow(
|
||||||
|
OtherSettings(false, false, false, false, false, false, false, false)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val dataSourcesSettings: MutableStateFlow<DataSourcesSettings> = MutableStateFlow(dataSources)
|
||||||
|
|
||||||
|
override val radioControlSettings: StateFlow<RadioControlSettings> = MutableStateFlow(
|
||||||
|
RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600)
|
||||||
|
)
|
||||||
|
|
||||||
|
val satelliteTypeIdsByType = mutableMapOf<String, List<Int>>()
|
||||||
|
|
||||||
|
override fun setSelectedIds(ids: List<Int>) = Unit
|
||||||
|
|
||||||
|
override fun setSelectedTypes(types: List<String>) = 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<String>): List<Int> = emptyList()
|
||||||
|
|
||||||
|
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
|
||||||
|
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 = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-2
@@ -18,8 +18,8 @@
|
|||||||
package com.rtbishop.look4sat.core.domain.repository
|
package com.rtbishop.look4sat.core.domain.repository
|
||||||
|
|
||||||
interface IDatabaseRepo {
|
interface IDatabaseRepo {
|
||||||
suspend fun updateTLEFromFile(uri: String)
|
suspend fun updateTLEFromFile(uri: String): Int
|
||||||
suspend fun updateTransceiversFromFile(uri: String)
|
suspend fun updateTransceiversFromFile(uri: String): Int
|
||||||
suspend fun updateFromRemote()
|
suspend fun updateFromRemote()
|
||||||
suspend fun clearAllData()
|
suspend fun clearAllData()
|
||||||
}
|
}
|
||||||
@@ -138,6 +138,8 @@
|
|||||||
<string name="prefs_data_clear">Clear</string>
|
<string name="prefs_data_clear">Clear</string>
|
||||||
<string name="prefs_data_clear_success">Data was cleared successfully</string>
|
<string name="prefs_data_clear_success">Data was cleared successfully</string>
|
||||||
<string name="prefs_data_update_success">Update completed successfully</string>
|
<string name="prefs_data_update_success">Update completed successfully</string>
|
||||||
|
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
||||||
|
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
||||||
|
|
||||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
<string name="prefs_data_sources_title">Custom data sources</string>
|
||||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
||||||
|
|||||||
+1
-1
@@ -161,7 +161,7 @@ fun DataSourcesDialog(
|
|||||||
onImportTle()
|
onImportTle()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
text = "TLE (3LE)\nR4UAB (.txt)",
|
text = "TLE/3LE (.txt)\nOMM (.csv)",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
CardButton(
|
CardButton(
|
||||||
|
|||||||
+6
-3
@@ -357,7 +357,7 @@ private fun LocationCard(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DataCardPreview() = MainTheme {
|
private fun DataCardPreview() = MainTheme {
|
||||||
val settings = DataSettings(true, 5000, 2500, 0L)
|
val settings = DataSettings(true, 5000, 2500, 0L)
|
||||||
DataCard(settings = settings, {}, {}, {})
|
DataCard(settings = settings, updateFromWeb = {}, clearAllData = {}, showDataSourcesDialog = {})
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -672,12 +672,15 @@ private fun rememberSettingsPermissions(
|
|||||||
else sendAction(SettingsAction.ShowToast(locationError))
|
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 ->
|
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 ->
|
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)
|
val bluetoothError = stringResource(R.string.prefs_bt_perm_error)
|
||||||
|
|||||||
+6
-3
@@ -28,7 +28,10 @@ data class PositionSettings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class DataSettings(
|
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(
|
data class SettingsState(
|
||||||
@@ -50,8 +53,8 @@ sealed interface SettingsAction {
|
|||||||
|
|
||||||
// Data
|
// Data
|
||||||
data object UpdateFromWeb : SettingsAction
|
data object UpdateFromWeb : SettingsAction
|
||||||
data class UpdateTLEFromFile(val uri: String) : SettingsAction
|
data class UpdateTLEFromFile(val uri: String, val invalidFileMessage: String) : SettingsAction
|
||||||
data class UpdateTransceiversFromFile(val uri: String) : SettingsAction
|
data class UpdateTransceiversFromFile(val uri: String, val invalidFileMessage: String) : SettingsAction
|
||||||
data object ClearAllData : SettingsAction
|
data object ClearAllData : SettingsAction
|
||||||
|
|
||||||
// Toggles
|
// Toggles
|
||||||
|
|||||||
+24
-10
@@ -65,11 +65,11 @@ class SettingsViewModel(
|
|||||||
settingsRepo.databaseState.collect { state ->
|
settingsRepo.databaseState.collect { state ->
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
dataSettings = DataSettings(
|
dataSettings = it.dataSettings.copy(
|
||||||
false,
|
isUpdating = false,
|
||||||
state.numberOfSatellites,
|
entriesTotal = state.numberOfSatellites,
|
||||||
state.numberOfRadios,
|
radiosTotal = state.numberOfRadios,
|
||||||
state.updateTimestamp
|
timestamp = state.updateTimestamp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -107,11 +107,11 @@ class SettingsViewModel(
|
|||||||
SettingsAction.DismissPosMessages -> dismissPosMessage()
|
SettingsAction.DismissPosMessages -> dismissPosMessage()
|
||||||
// Data
|
// Data
|
||||||
SettingsAction.UpdateFromWeb -> runDataUpdate { databaseRepo.updateFromRemote() }
|
SettingsAction.UpdateFromWeb -> runDataUpdate { databaseRepo.updateFromRemote() }
|
||||||
is SettingsAction.UpdateTLEFromFile -> runDataUpdate { databaseRepo.updateTLEFromFile(action.uri) }
|
is SettingsAction.UpdateTLEFromFile -> runManualImport(action.invalidFileMessage) {
|
||||||
is SettingsAction.UpdateTransceiversFromFile -> runDataUpdate {
|
databaseRepo.updateTLEFromFile(action.uri)
|
||||||
databaseRepo.updateTransceiversFromFile(
|
}
|
||||||
action.uri
|
is SettingsAction.UpdateTransceiversFromFile -> runManualImport(action.invalidFileMessage) {
|
||||||
)
|
databaseRepo.updateTransceiversFromFile(action.uri)
|
||||||
}
|
}
|
||||||
SettingsAction.ClearAllData -> viewModelScope.launch { databaseRepo.clearAllData() }
|
SettingsAction.ClearAllData -> viewModelScope.launch { databaseRepo.clearAllData() }
|
||||||
// Toggles
|
// 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
|
// endregion
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun factory(container: IMainContainer) = viewModelFactory {
|
fun factory(container: IMainContainer) = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
SettingsViewModel(
|
SettingsViewModel(
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ jdkVersion = "21"
|
|||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
packageName = "com.rtbishop.look4sat"
|
packageName = "com.rtbishop.look4sat"
|
||||||
|
|
||||||
android-gradle-plugin = "9.2.1"
|
android-gradle-plugin = "9.3.1"
|
||||||
|
|
||||||
androidx-core-ktx = "1.19.0"
|
androidx-core-ktx = "1.19.0"
|
||||||
androidx-core-splashscreen = "1.2.0"
|
androidx-core-splashscreen = "1.2.0"
|
||||||
androidx-room = "2.8.4"
|
androidx-room = "2.8.4"
|
||||||
|
|
||||||
compose-bom = "2026.06.00"
|
compose-bom = "2026.06.01"
|
||||||
compose-activity = "1.13.0"
|
compose-activity = "1.13.0"
|
||||||
compose-lifecycle = "2.11.0"
|
compose-lifecycle = "2.11.0"
|
||||||
compose-navigation3 = "1.1.3"
|
compose-navigation3 = "1.1.5"
|
||||||
|
|
||||||
google-ksp = "2.3.8"
|
google-ksp = "2.3.8"
|
||||||
|
|
||||||
kotlin = "2.4.0"
|
kotlin = "2.4.10"
|
||||||
kotlin-coroutines = "1.11.0"
|
kotlin-coroutines = "1.11.0"
|
||||||
kotlin-serialization = "1.11.0"
|
kotlin-serialization = "1.11.0"
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user