Further classes restructuring and code cleanup

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:38 +00:00
1 parent f6fcd21404
commit a807cfd8f6
20 files changed
+262 -299

No files matched your search

-1
View File
@@ -65,7 +65,6 @@ dependencies {
implementation "androidx.compose.compiler:compiler:$compose_compiler_version"
implementation "androidx.compose.material3:material3:$material3_version"
implementation "androidx.compose.runtime:runtime:$compose_version"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
implementation "androidx.navigation:navigation-compose:$navigation_version"
@@ -18,6 +18,7 @@
package com.rtbishop.look4sat
import android.app.Application
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -29,13 +30,13 @@ class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
container = MainContainer(this)
checkAutoUpdate()
container.mainScope.launch { checkAutoUpdate() }
}
private fun checkAutoUpdate() {
private suspend fun checkAutoUpdate() {
val settingsRepo = container.settingsRepo
if (settingsRepo.isUpdateEnabled()) {
val timeDelta = System.currentTimeMillis() - settingsRepo.getLastUpdateTime()
if (settingsRepo.otherSettings.value.updateState) {
val timeDelta = System.currentTimeMillis() - settingsRepo.databaseState.value.timestamp
if (timeDelta > 172800000) { // 48 hours in ms
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
println("Started periodic data update on ${sdf.format(Date())}")
@@ -23,6 +23,7 @@ import com.rtbishop.look4sat.framework.data.DataSource
import com.rtbishop.look4sat.framework.data.LocalStorage
import com.rtbishop.look4sat.framework.data.MIGRATION_1_2
import com.rtbishop.look4sat.framework.data.MainDatabase
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -33,10 +34,13 @@ class MainContainer(private val context: Context) {
private val database = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb")
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
private val localStorage = LocalStorage(database.storageDao())
private val mainHandler = CoroutineExceptionHandler { _, error -> println("Look4Sat: $error") }
val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
val settingsRepo = provideSettingsRepo()
val databaseRepo = provideDatabaseRepo(settingsRepo)
val sensorsRepo = provideSensorRepo()
val databaseRepo = provideDatabaseRepo()
val satelliteRepo = provideSatelliteRepo()
val selectionRepo = provideSelectionRepo()
val sensorsRepo = provideSensorsRepo()
fun provideBluetoothReporter(): BluetoothReporter {
val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
@@ -47,27 +51,26 @@ class MainContainer(private val context: Context) {
return NetworkReporter(CoroutineScope(Dispatchers.IO))
}
private fun provideDatabaseRepo(settingsRepo: ISettingsRepo): IDatabaseRepo {
private fun provideDatabaseRepo(): IDatabaseRepo {
val dataParser = DataParser(Dispatchers.Default)
val dataSource = DataSource(context.contentResolver, Dispatchers.IO)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
return DatabaseRepo(dataParser, localStorage, dataSource, scope, settingsRepo)
}
fun provideSelectionRepo(): ISelectionRepo {
return SelectionRepo(Dispatchers.Default, localStorage, settingsRepo)
}
private fun provideSensorRepo(): ISensorsRepo {
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return SensorsRepo(sensorManager, sensor)
return DatabaseRepo(Dispatchers.Default, dataParser, dataSource, localStorage, settingsRepo)
}
private fun provideSatelliteRepo(): ISatelliteRepo {
return SatelliteRepo(Dispatchers.Default, localStorage)
}
private fun provideSelectionRepo(): ISelectionRepo {
return SelectionRepo(Dispatchers.Default, localStorage, settingsRepo)
}
private fun provideSensorsRepo(): ISensorsRepo {
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return SensorsRepo(sensorManager, sensor)
}
private fun provideSettingsRepo(): ISettingsRepo {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
@@ -24,7 +24,9 @@ import androidx.core.content.edit
import androidx.core.location.LocationManagerCompat
import androidx.core.os.CancellationSignal
import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.model.DatabaseState
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.OtherSettings
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round
import kotlinx.coroutines.flow.MutableStateFlow
@@ -36,11 +38,13 @@ class SettingsRepo(
) : ISettingsRepo {
private val keyModes = "satModes"
private val keyCompass = "compass"
private val keyRadarSweep = "radarSweep"
private val keyTimeUTC = "timeUTC"
private val keyLastUpdateTime = "lastUpdateTime"
private val keyAutoUpdateEnabled = "autoUpdateEnabled"
private val keyUtcState = "utcState"
private val keyUpdateState = "updateState"
private val keySweepState = "sweepState"
private val keySensorState = "sensorState"
private val keyDataEntries = "dataEntries"
private val keyDataRadios = "dataRadios"
private val keyDataTimestamp = "dataTimestamp"
private val keyHoursAhead = "hoursAhead"
private val keyMinElevation = "minElevation"
private val keyRotator = "isRotatorEnabled"
@@ -164,44 +168,52 @@ class SettingsRepo(
preferences.edit { putDouble(keyMinElevation, minElevation) }
}
override fun isUtcEnabled(): Boolean {
return preferences.getBoolean(keyTimeUTC, false)
private val _databaseState = MutableStateFlow(loadDatabaseState())
override val databaseState: StateFlow<DatabaseState> = _databaseState
override fun saveDatabaseState(state: DatabaseState) = preferences.edit {
putInt(keyDataEntries, state.entriesTotal)
putInt(keyDataRadios, state.radiosTotal)
putLong(keyDataTimestamp, state.timestamp)
_databaseState.value = state
}
override fun setUtcState(value: Boolean) {
preferences.edit { putBoolean(keyTimeUTC, value) }
private fun loadDatabaseState(): DatabaseState {
val entriesTotal = preferences.getInt(keyDataEntries, 0)
val radiosTotal = preferences.getInt(keyDataRadios, 0)
val timestamp = preferences.getLong(keyDataTimestamp, 0L)
return DatabaseState(entriesTotal, radiosTotal, timestamp)
}
override fun getLastUpdateTime(): Long {
return preferences.getLong(keyLastUpdateTime, 0L)
private val _otherSettings = MutableStateFlow(loadOtherSettings())
override val otherSettings: StateFlow<OtherSettings> = _otherSettings
override fun toggleUtc(value: Boolean) {
preferences.edit { putBoolean(keyUtcState, value) }
_otherSettings.value = otherSettings.value.copy(utcState = value)
}
override fun setLastUpdateTime(updateTime: Long) {
preferences.edit { putLong(keyLastUpdateTime, updateTime) }
override fun toggleUpdate(value: Boolean) {
preferences.edit { putBoolean(keyUpdateState, value) }
_otherSettings.value = otherSettings.value.copy(updateState = value)
}
override fun isUpdateEnabled(): Boolean {
return preferences.getBoolean(keyAutoUpdateEnabled, true)
override fun toggleSweep(value: Boolean) {
preferences.edit { putBoolean(keySweepState, value) }
_otherSettings.value = otherSettings.value.copy(sweepState = value)
}
override fun setUpdateState(value: Boolean) {
preferences.edit { putBoolean(keyAutoUpdateEnabled, value) }
override fun toggleSensor(value: Boolean) {
preferences.edit { putBoolean(keySensorState, value) }
_otherSettings.value = otherSettings.value.copy(sensorState = value)
}
override fun isSensorEnabled(): Boolean {
return preferences.getBoolean(keyCompass, true)
}
override fun setSensorState(value: Boolean) {
preferences.edit { putBoolean(keyCompass, value) }
}
override fun isSweepEnabled(): Boolean {
return preferences.getBoolean(keyRadarSweep, true)
}
override fun setSweepState(value: Boolean) {
preferences.edit { putBoolean(keyRadarSweep, value) }
private fun loadOtherSettings(): OtherSettings {
val utcState = preferences.getBoolean(keyUtcState, false)
val updateState = preferences.getBoolean(keyUpdateState, true)
val sweepState = preferences.getBoolean(keySweepState, true)
val sensorState = preferences.getBoolean(keySensorState, true)
return OtherSettings(utcState, updateState, sweepState, sensorState)
}
override fun saveModesSelection(modes: List<String>) {
@@ -31,7 +31,7 @@ class LocalStorage(private val storageDao: StorageDao) : ILocalStorage {
//region # Entries region
override fun getEntriesTotal() = storageDao.getEntriesTotal()
override suspend fun getEntriesTotal() = storageDao.getEntriesTotal()
override suspend fun getEntriesList(): List<DomainItem> {
return storageDao.getEntriesList().toDomainItems()
@@ -64,7 +64,7 @@ class LocalStorage(private val storageDao: StorageDao) : ILocalStorage {
//region # Radios region
override fun getRadiosTotal() = storageDao.getRadiosTotal()
override suspend fun getRadiosTotal() = storageDao.getRadiosTotal()
override suspend fun getRadiosWithId(id: Int): List<SatRadio> {
return storageDao.getRadiosWithId(id).toDomainRadios()
@@ -24,7 +24,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatRadio
@Database(entities = [SatEntry::class, SatRadio::class], version = 2)
@Database(entities = [SatEntry::class, SatRadio::class], version = 2, exportSchema = false)
abstract class MainDatabase : RoomDatabase() {
abstract fun storageDao(): StorageDao
@@ -21,13 +21,12 @@ import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatItem
import com.rtbishop.look4sat.framework.model.SatRadio
import kotlinx.coroutines.flow.Flow
@Dao
interface StorageDao {
@Query("SELECT COUNT(*) FROM entries")
fun getEntriesTotal(): Flow<Int>
suspend fun getEntriesTotal(): Int
//@Transaction
@Query("SELECT catnum, name FROM entries ORDER BY name ASC")
@@ -44,7 +43,7 @@ interface StorageDao {
suspend fun deleteEntries()
@Query("SELECT COUNT(*) FROM radios")
fun getRadiosTotal(): Flow<Int>
suspend fun getRadiosTotal(): Int
@Transaction
@Query("SELECT * FROM radios WHERE catnum = :id AND isAlive = 1")
@@ -69,7 +69,7 @@ class EntriesViewModel(private val selectionRepo: ISelectionRepo) : ViewModel()
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
initializer {
val container = (this[applicationKey] as MainApplication).container
EntriesViewModel(container.provideSelectionRepo())
EntriesViewModel(container.selectionRepo)
}
}
}
@@ -89,7 +89,7 @@ class PassesViewModel(
}
}
fun shouldUseUTC() = settingsRepo.isUtcEnabled()
fun shouldUseUTC() = settingsRepo.otherSettings.value.utcState
fun calculatePasses(
hoursAhead: Int = 1,
@@ -61,7 +61,7 @@ class RadarViewModel(
val orientation = mutableStateOf(sensorsRepo.orientation.value)
init {
if (settingsRepo.isSensorEnabled()) {
if (settingsRepo.otherSettings.value.sensorState) {
viewModelScope.launch {
sensorsRepo.enableSensor()
sensorsRepo.orientation.collect { data ->
@@ -100,9 +100,9 @@ class RadarViewModel(
}
}
fun getUseCompass(): Boolean = settingsRepo.isSensorEnabled()
fun getUseCompass(): Boolean = settingsRepo.otherSettings.value.sensorState
fun getShowSweep(): Boolean = settingsRepo.isSweepEnabled()
fun getShowSweep(): Boolean = settingsRepo.otherSettings.value.sweepState
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.latitude.toFloat()
@@ -38,6 +38,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.OtherSettings
import com.rtbishop.look4sat.presentation.CardButton
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.dialogs.LocatorDialog
@@ -76,43 +77,55 @@ fun SettingsScreen() {
val locationPermFine = Manifest.permission.ACCESS_FINE_LOCATION
val locationRequest = rememberLauncherForActivityResult(locationContract) { permissions ->
when {
permissions[locationPermFine] == true -> viewModel.locationSettings.value.setGpsLoc()
permissions[locationPermCoarse] == true -> viewModel.locationSettings.value.setGpsLoc()
permissions[locationPermFine] == true -> viewModel.setGpsPosition()
permissions[locationPermCoarse] == true -> viewModel.setGpsPosition()
else -> showToast(context, locationError)
}
}
val contentContract = ActivityResultContracts.GetContent()
val contentRequest = rememberLauncherForActivityResult(contentContract) { uri ->
uri?.let { viewModel.dataSettings.value.updateFromFile(uri.toString()) }
uri?.let { viewModel.updateFromFile(uri.toString()) }
}
// Location settings
val locSettings = viewModel.locationSettings.value
val setGpsLoc = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) }
val showPosDialog = rememberSaveable { mutableStateOf(false) }
val togglePosDialog = { showPosDialog.value = showPosDialog.value.not() }
if (showPosDialog.value) {
PositionDialog(
locSettings.stationPos.latitude,
locSettings.stationPos.longitude,
togglePosDialog,
locSettings.setManualLoc
)
// Position settings
val positionSettings = viewModel.positionSettings.value
val stationPos = positionSettings.stationPos
val setGpsPos = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) }
val setGeoPos = { lat: Double, lon: Double -> viewModel.setGeoPosition(lat, lon) }
val setQthPos = { locator: String -> viewModel.setQthPosition(locator) }
val dismissPos = { viewModel.dismissPosMessage() }
val posDialogState = rememberSaveable { mutableStateOf(false) }
val showPosDialog = { posDialogState.value = posDialogState.value.not() }
if (posDialogState.value) {
PositionDialog(stationPos.latitude, stationPos.longitude, showPosDialog, setGeoPos)
}
val showLocDialog = rememberSaveable { mutableStateOf(false) }
val toggleLocDialog = { showLocDialog.value = showLocDialog.value.not() }
if (showLocDialog.value) {
LocatorDialog(locSettings.stationPos.qthLocator, toggleLocDialog, locSettings.setQthLoc)
val locDialogState = rememberSaveable { mutableStateOf(false) }
val showLocDialog = { locDialogState.value = locDialogState.value.not() }
if (locDialogState.value) {
LocatorDialog(positionSettings.stationPos.qthLocator, showLocDialog, setQthPos)
}
// Data settings
val dataSettings = viewModel.dataSettings.value
val updateFromWeb: () -> Unit = { viewModel.updateFromWeb() }
val updateFromFile = { contentRequest.launch("*/*") }
val clearAllData: () -> Unit = { viewModel.clearAllData() }
// Other settings
val otherSettings = viewModel.otherSettings.value
val toggleUtc = { value: Boolean -> viewModel.toggleUtc(value) }
val toggleUpdate = { value: Boolean -> viewModel.toggleUpdate(value) }
val toggleSweep = { value: Boolean -> viewModel.toggleSweep(value) }
val toggleSensor = { value: Boolean -> viewModel.toggleSensor(value) }
// Screen setup
LazyColumn(
modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)
) {
item { CardAbout(BuildConfig.VERSION_NAME) }
item { LocationCard(locSettings, setGpsLoc, togglePosDialog, toggleLocDialog) }
item { DataCard(viewModel.dataSettings.value) { contentRequest.launch("*/*") } }
item { OtherCard(viewModel.otherSettings.value) }
item { LocationCard(positionSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos) }
item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) }
item { OtherCard(otherSettings, toggleUtc, toggleUpdate, toggleSweep, toggleSensor) }
item { CardCredits() }
}
}
@@ -181,16 +194,17 @@ private fun CardAbout(version: String, modifier: Modifier = Modifier) {
@Composable
private fun LocationCardPreview() = MainTheme {
val stationPos = GeoPos(0.0, 0.0, 0.0, "IO91vl", 0L)
val settings = LocationSettings(true, stationPos, 0, {}, { _, _ -> }, {}, {})
LocationCard(settings = settings, setGpsLoc = {}, togglePosDialog = {}) {}
val settings = PositionSettings(true, stationPos, 0)
LocationCard(settings = settings, setGpsPos = {}, showPosDialog = {}, {}) {}
}
@Composable
private fun LocationCard(
settings: LocationSettings,
setGpsLoc: () -> Unit,
togglePosDialog: () -> Unit,
toggleLocDialog: () -> Unit
settings: PositionSettings,
setGpsPos: () -> Unit,
showPosDialog: () -> Unit,
showLocDialog: () -> Unit,
dismissPosMessage: () -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(
@@ -227,19 +241,19 @@ private fun LocationCard(
Spacer(modifier = Modifier.height(1.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
CardButton(
onClick = setGpsLoc,
onClick = setGpsPos,
text = stringResource(id = R.string.btn_gps),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = togglePosDialog,
onClick = showPosDialog,
text = stringResource(id = R.string.btn_manual),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = toggleLocDialog,
onClick = showLocDialog,
text = stringResource(id = R.string.btn_qth),
modifier = Modifier.weight(1f)
)
@@ -251,7 +265,7 @@ private fun LocationCard(
val errorString = stringResource(id = settings.messageResId)
LaunchedEffect(key1 = settings.messageResId) {
showToast(context, errorString)
settings.dismissMessage()
dismissPosMessage()
}
}
}
@@ -259,12 +273,17 @@ private fun LocationCard(
@Preview(showBackground = true)
@Composable
private fun DataCardPreview() = MainTheme {
val settings = DataSettings(true, 0L, 5000, 2500, {}, {}, {})
DataCard(settings = settings) {}
val settings = DataSettings(true, 5000, 2500, 0L)
DataCard(settings = settings, {}, {}) {}
}
@Composable
private fun DataCard(settings: DataSettings, updateFromFile: () -> Unit) {
private fun DataCard(
settings: DataSettings,
updateFromWeb: () -> Unit,
updateFromFile: () -> Unit,
clearAllData: () -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -278,29 +297,29 @@ private fun DataCard(settings: DataSettings, updateFromFile: () -> Unit) {
UpdateIndicator(isUpdating = settings.isUpdating, Modifier.weight(1f))
}
Spacer(modifier = Modifier.height(2.dp))
Text(text = setUpdateTime(updateTime = settings.lastUpdated))
Text(text = setUpdateTime(updateTime = settings.timestamp))
Spacer(modifier = Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Text(text = "Satellites: ${settings.satsTotal}")
Text(text = "Satellites: ${settings.entriesTotal}")
Spacer(modifier = Modifier.weight(1f))
Text(text = "Transceivers: ${settings.radiosTotal}")
}
Spacer(modifier = Modifier.height(1.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
CardButton(
onClick = settings.updateFromWeb,
onClick = { updateFromWeb() },
text = stringResource(id = R.string.btn_web),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = updateFromFile,
onClick = { updateFromFile() },
text = stringResource(id = R.string.btn_file),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = settings.clearAllData,
onClick = { clearAllData() },
text = stringResource(id = R.string.btn_clear),
modifier = Modifier.weight(1f)
)
@@ -357,14 +376,19 @@ private fun DataCard(settings: DataSettings, updateFromFile: () -> Unit) {
@Preview(showBackground = true)
@Composable
private fun OtherCardPreview() = MainTheme {
val settings = OtherSettings(
getUtc = false, getUpdate = true, getSweep = true, getSensor = true,
setUtc = {}, setUpdate = {}, setSweep = {}, setSensor = {})
OtherCard(settings = settings)
val values =
OtherSettings(utcState = false, updateState = true, sweepState = true, sensorState = true)
OtherCard(settings = values, {}, {}, {}, {})
}
@Composable
private fun OtherCard(settings: OtherSettings) {
private fun OtherCard(
settings: OtherSettings,
toggleUtc: (Boolean) -> Unit,
toggleUpdate: (Boolean) -> Unit,
toggleSweep: (Boolean) -> Unit,
toggleSensor: (Boolean) -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)) {
Row(
@@ -373,7 +397,7 @@ private fun OtherCard(settings: OtherSettings) {
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.other_switch_utc))
Switch(checked = settings.getUtc, onCheckedChange = { settings.setUtc(it) })
Switch(checked = settings.utcState, onCheckedChange = { toggleUtc(it) })
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
@@ -381,7 +405,7 @@ private fun OtherCard(settings: OtherSettings) {
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.other_switch_update))
Switch(checked = settings.getUpdate, onCheckedChange = { settings.setUpdate(it) })
Switch(checked = settings.updateState, onCheckedChange = { toggleUpdate(it) })
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
@@ -389,7 +413,7 @@ private fun OtherCard(settings: OtherSettings) {
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.other_switch_sweep))
Switch(checked = settings.getSweep, onCheckedChange = { settings.setSweep(it) })
Switch(checked = settings.sweepState, onCheckedChange = { toggleSweep(it) })
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
@@ -397,7 +421,7 @@ private fun OtherCard(settings: OtherSettings) {
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.other_switch_sensors))
Switch(checked = settings.getSensor, onCheckedChange = { settings.setSensor(it) })
Switch(checked = settings.sensorState, onCheckedChange = { toggleSensor(it) })
}
}
}
@@ -2,33 +2,8 @@ package com.rtbishop.look4sat.presentation.settings
import com.rtbishop.look4sat.model.GeoPos
data class LocationSettings(
val isUpdating: Boolean,
val stationPos: GeoPos,
val messageResId: Int,
val setGpsLoc: () -> Unit,
val setManualLoc: (Double, Double) -> Unit,
val setQthLoc: (String) -> Unit,
val dismissMessage: () -> Unit
)
data class PositionSettings(val isUpdating: Boolean, val stationPos: GeoPos, val messageResId: Int)
data class DataSettings(
val isUpdating: Boolean,
val lastUpdated: Long,
val satsTotal: Int,
val radiosTotal: Int,
val updateFromWeb: () -> Unit,
val updateFromFile: (String) -> Unit,
val clearAllData: () -> Unit
)
data class OtherSettings(
val getUtc: Boolean,
val getUpdate: Boolean,
val getSweep: Boolean,
val getSensor: Boolean,
val setUtc: (Boolean) -> Unit,
val setUpdate: (Boolean) -> Unit,
val setSweep: (Boolean) -> Unit,
val setSensor: (Boolean) -> Unit
val isUpdating: Boolean, val entriesTotal: Int, val radiosTotal: Int, val timestamp: Long
)
@@ -17,6 +17,7 @@
*/
package com.rtbishop.look4sat.presentation.settings
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -27,121 +28,107 @@ import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.IDatabaseRepo
import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.model.DataState
import kotlinx.coroutines.flow.combine
import com.rtbishop.look4sat.model.OtherSettings
import kotlinx.coroutines.launch
class SettingsViewModel(
private val databaseRepo: IDatabaseRepo, private val settingsRepo: ISettingsRepo
) : ViewModel() {
private val defaultLocationSettings = LocationSettings(false,
settingsRepo.stationPosition.value,
0,
{ setGpsPosition() },
{ latitude, longitude -> setGeoPosition(latitude, longitude) },
{ locator -> setQthPosition(locator) },
{ dismissLocationMessage() })
val locationSettings = mutableStateOf(defaultLocationSettings)
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
private val _positionSettings = mutableStateOf(defaultPosSettings)
val positionSettings: State<PositionSettings> = _positionSettings
private val defaultDataSettings = DataSettings(false,
settingsRepo.getLastUpdateTime(),
0,
0,
{ databaseRepo.updateFromWeb() },
{ databaseRepo.updateFromFile(it) },
{ databaseRepo.clearAllData() })
val dataSettings = mutableStateOf(defaultDataSettings)
private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
private val _dataSettings = mutableStateOf(defaultDataSettings)
val dataSettings: State<DataSettings> = _dataSettings
private val defaultOtherSettings = OtherSettings(settingsRepo.isUtcEnabled(),
settingsRepo.isUpdateEnabled(),
settingsRepo.isSweepEnabled(),
settingsRepo.isSensorEnabled(),
{ setUtc(it) },
{ setUpdate(it) },
{ setSweep(it) },
{ setSensor(it) })
val otherSettings = mutableStateOf(defaultOtherSettings)
private val _otherSettings = mutableStateOf(settingsRepo.otherSettings.value)
val otherSettings: State<OtherSettings> = _otherSettings
init {
viewModelScope.launch {
settingsRepo.stationPosition.collect { stationPos ->
locationSettings.value = locationSettings.value.copy(
isUpdating = false, stationPos = stationPos
settingsRepo.stationPosition.collect { geoPos ->
_positionSettings.value =
_positionSettings.value.copy(isUpdating = false, stationPos = geoPos)
}
}
viewModelScope.launch {
settingsRepo.databaseState.collect { state ->
_dataSettings.value = _dataSettings.value.copy(
isUpdating = false,
entriesTotal = state.entriesTotal,
radiosTotal = state.radiosTotal,
timestamp = state.timestamp
)
}
}
viewModelScope.launch {
databaseRepo.updateState.collect {
val isUpdating = it is DataState.Loading
dataSettings.value = dataSettings.value.copy(
isUpdating = isUpdating, lastUpdated = settingsRepo.getLastUpdateTime()
)
}
}
viewModelScope.launch {
combine(
databaseRepo.getEntriesTotal(), databaseRepo.getRadiosTotal()
) { sats, radios ->
dataSettings.value = dataSettings.value.copy(satsTotal = sats, radiosTotal = radios)
}.collect {}
settingsRepo.otherSettings.collect { _otherSettings.value = it }
}
}
private fun setGpsPosition() {
fun setGpsPosition() {
if (settingsRepo.setGpsPosition()) {
val messageResId = R.string.location_success
locationSettings.value =
locationSettings.value.copy(isUpdating = true, messageResId = messageResId)
_positionSettings.value =
_positionSettings.value.copy(isUpdating = true, messageResId = messageResId)
} else {
val errorResId = R.string.location_gps_error
locationSettings.value = locationSettings.value.copy(messageResId = errorResId)
_positionSettings.value = _positionSettings.value.copy(messageResId = errorResId)
}
}
private fun setGeoPosition(latitude: Double, longitude: Double) {
fun setGeoPosition(latitude: Double, longitude: Double) {
if (settingsRepo.setGeoPosition(latitude, longitude)) {
val messageResId = R.string.location_success
locationSettings.value = locationSettings.value.copy(messageResId = messageResId)
_positionSettings.value = _positionSettings.value.copy(messageResId = messageResId)
} else {
val errorResId = R.string.location_manual_error
locationSettings.value = locationSettings.value.copy(messageResId = errorResId)
_positionSettings.value = _positionSettings.value.copy(messageResId = errorResId)
}
}
private fun setQthPosition(locator: String) {
fun setQthPosition(locator: String) {
if (settingsRepo.setQthPosition(locator)) {
val messageResId = R.string.location_success
locationSettings.value = locationSettings.value.copy(messageResId = messageResId)
_positionSettings.value = _positionSettings.value.copy(messageResId = messageResId)
} else {
val errorResId = R.string.location_qth_error
locationSettings.value = locationSettings.value.copy(messageResId = errorResId)
_positionSettings.value = _positionSettings.value.copy(messageResId = errorResId)
}
}
private fun dismissLocationMessage() {
locationSettings.value = locationSettings.value.copy(messageResId = 0)
fun dismissPosMessage() {
_positionSettings.value = _positionSettings.value.copy(messageResId = 0)
}
private fun setUtc(value: Boolean) {
settingsRepo.setUtcState(value)
otherSettings.value = otherSettings.value.copy(getUtc = value)
fun updateFromWeb() = viewModelScope.launch {
try {
_dataSettings.value = _dataSettings.value.copy(isUpdating = true)
databaseRepo.updateFromWeb()
} catch (exception: Exception) {
_dataSettings.value = _dataSettings.value.copy(isUpdating = false)
println(exception.printStackTrace())
}
}
private fun setUpdate(value: Boolean) {
settingsRepo.setUpdateState(value)
otherSettings.value = otherSettings.value.copy(getUpdate = value)
fun updateFromFile(uri: String) = viewModelScope.launch {
try {
_dataSettings.value = _dataSettings.value.copy(isUpdating = true)
databaseRepo.updateFromFile(uri)
} catch (exception: Exception) {
_dataSettings.value = _dataSettings.value.copy(isUpdating = false)
println(exception.printStackTrace())
}
}
private fun setSweep(value: Boolean) {
settingsRepo.setSweepState(value)
otherSettings.value = otherSettings.value.copy(getSweep = value)
}
fun clearAllData() = viewModelScope.launch { databaseRepo.clearAllData() }
private fun setSensor(value: Boolean) {
settingsRepo.setSensorState(value)
otherSettings.value = otherSettings.value.copy(getSensor = value)
}
fun toggleUtc(value: Boolean) = settingsRepo.toggleUtc(value)
fun toggleUpdate(value: Boolean) = settingsRepo.toggleUpdate(value)
fun toggleSweep(value: Boolean) = settingsRepo.toggleSweep(value)
fun toggleSensor(value: Boolean) = settingsRepo.toggleSensor(value)
// fun getRotatorEnabled(): Boolean = settings.getRotatorEnabled()
// fun setRotatorEnabled(value: Boolean) = settings.setRotatorEnabled(value)
@@ -19,58 +19,41 @@ package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.IDatabaseRepo
import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.DatabaseState
import com.rtbishop.look4sat.model.SatEntry
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.zip.ZipInputStream
class DatabaseRepo(
private val dispatcher: CoroutineDispatcher,
private val dataParser: DataParser,
private val localStorage: ILocalStorage,
private val dataSource: IDataSource,
private val repositoryScope: CoroutineScope,
private val localStorage: ILocalStorage,
private val settingsRepository: ISettingsRepo
) : IDatabaseRepo {
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
_updateState.value = DataState.Exception(exception.message)
}
private val updateStateDelay = 875L
private val _updateState = MutableStateFlow<DataState<Long>>(DataState.Handled)
override val updateState: StateFlow<DataState<Long>> = _updateState
override fun getEntriesTotal() = localStorage.getEntriesTotal()
override fun getRadiosTotal() = localStorage.getRadiosTotal()
override fun updateFromFile(uri: String) {
repositoryScope.launch(exceptionHandler) {
_updateState.value = DataState.Loading
dataSource.getFileStream(uri)?.let { stream ->
delay(updateStateDelay)
localStorage.insertEntries(importSatellites(stream))
}
setUpdateSuccessful()
}
override suspend fun updateFromFile(uri: String) = withContext(dispatcher) {
val importedSatellites = dataSource.getFileStream(uri)?.let { importSatellites(it) }
importedSatellites?.let { localStorage.insertEntries(it) }
setUpdateSuccessful(System.currentTimeMillis())
}
override fun updateFromWeb() {
_updateState.value = DataState.Loading
repositoryScope.launch(exceptionHandler) {
val importedEntries = mutableListOf<SatEntry>()
override suspend fun updateFromWeb() = withContext(dispatcher) {
val sourcesMap = settingsRepository.satelliteSourcesMap
val importedEntries = mutableListOf<SatEntry>()
val importedRadios = mutableListOf<SatRadio>()
// fetch
val jobsMap = sourcesMap.mapValues { async { dataSource.getNetworkStream(it.value) } }
val jobRadios = async { dataSource.getNetworkStream(settingsRepository.radioSourceUrl) }
// parse
jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream ->
when (val type = entry.key) {
"Amsat", "R4UAB" -> {
"AMSAT", "R4UAB" -> {
// parse tle stream
val satellites = importSatellites(stream)
val catnums = satellites.map { it.data.catnum }
@@ -98,33 +81,23 @@ class DatabaseRepo(
}
}
}
jobRadios.await()?.let { importedRadios.addAll(dataParser.parseJSONStream(it)) }
// insert
localStorage.insertEntries(importedEntries)
setUpdateSuccessful()
}
repositoryScope.launch(exceptionHandler) {
dataSource.getNetworkStream(settingsRepository.radioSourceUrl)?.let { stream ->
localStorage.insertRadios(dataParser.parseJSONStream(stream))
}
}
localStorage.insertRadios(importedRadios)
setUpdateSuccessful(System.currentTimeMillis())
}
override fun setUpdateStateHandled() {
_updateState.value = DataState.Handled
}
override fun clearAllData() {
repositoryScope.launch {
_updateState.value = DataState.Loading
delay(updateStateDelay)
override suspend fun clearAllData() = withContext(dispatcher) {
localStorage.deleteEntries()
localStorage.deleteRadios()
setUpdateSuccessful(0L)
}
}
private fun setUpdateSuccessful(updateTime: Long = System.currentTimeMillis()) {
settingsRepository.setLastUpdateTime(updateTime)
_updateState.value = DataState.Success(updateTime)
private suspend fun setUpdateSuccessful(timestamp: Long) = withContext(dispatcher) {
val satellitesTotal = localStorage.getEntriesTotal()
val radiosTotal = localStorage.getRadiosTotal()
settingsRepository.saveDatabaseState(DatabaseState(satellitesTotal, radiosTotal, timestamp))
}
private suspend fun importSatellites(stream: InputStream): List<SatEntry> {
@@ -21,11 +21,10 @@ import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.model.SatEntry
import com.rtbishop.look4sat.model.SatItem
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.Flow
interface ILocalStorage {
fun getEntriesTotal(): Flow<Int>
suspend fun getEntriesTotal(): Int
suspend fun getEntriesList(): List<SatItem>
@@ -35,7 +34,7 @@ interface ILocalStorage {
suspend fun deleteEntries()
fun getRadiosTotal(): Flow<Int>
suspend fun getRadiosTotal(): Int
suspend fun getRadiosWithId(id: Int): List<SatRadio>
@@ -17,23 +17,11 @@
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.DataState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface IDatabaseRepo {
val updateState: StateFlow<DataState<Long>>
suspend fun updateFromFile(uri: String)
fun getEntriesTotal(): Flow<Int>
suspend fun updateFromWeb()
fun getRadiosTotal(): Flow<Int>
fun updateFromFile(uri: String)
fun updateFromWeb()
fun setUpdateStateHandled()
fun clearAllData()
suspend fun clearAllData()
}
@@ -17,7 +17,9 @@
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.DatabaseState
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.OtherSettings
import kotlinx.coroutines.flow.StateFlow
interface ISettingsRepo {
@@ -72,25 +74,19 @@ interface ISettingsRepo {
fun setMinElevation(minElevation: Double)
fun isUtcEnabled(): Boolean
val databaseState: StateFlow<DatabaseState>
fun setUtcState(value: Boolean)
fun saveDatabaseState(state: DatabaseState)
fun getLastUpdateTime(): Long
val otherSettings: StateFlow<OtherSettings>
fun setLastUpdateTime(updateTime: Long)
fun toggleUtc(value: Boolean)
fun isUpdateEnabled(): Boolean
fun toggleUpdate(value: Boolean)
fun setUpdateState(value: Boolean)
fun toggleSweep(value: Boolean)
fun isSensorEnabled(): Boolean
fun setSensorState(value: Boolean)
fun isSweepEnabled(): Boolean
fun setSweepState(value: Boolean)
fun toggleSensor(value: Boolean)
fun saveModesSelection(modes: List<String>)
@@ -0,0 +1,10 @@
package com.rtbishop.look4sat.model
data class OtherSettings(
val utcState: Boolean,
val updateState: Boolean,
val sweepState: Boolean,
val sensorState: Boolean
)
data class DatabaseState(val entriesTotal: Int, val radiosTotal: Int, val timestamp: Long)
+5 -5
View File
@@ -6,13 +6,13 @@ buildscript {
kotlin_android_version = '1.8.10'
// Android
splashscreen_version = '1.0.0'
room_version = '2.5.0'
room_version = '2.5.1'
// Compose
activity_compose_version = '1.6.1'
compose_version = '1.3.3'
compose_compiler_version = '1.4.3'
activity_compose_version = '1.7.0'
compose_version = '1.4.0'
compose_compiler_version = '1.4.4'
material3_version = '1.0.1'
lifecycle_version = '2.6.0'
lifecycle_version = '2.6.1'
navigation_version = '2.5.3'
// Utility
osmdroid_version = '6.1.14'
-3
View File
@@ -24,9 +24,6 @@
android.enableJetifier=false
android.nonTransitiveRClass=true
android.useAndroidX=true
kapt.include.compile.classpath=true
kapt.incremental.apt=true
kapt.use.worker.api=true
kotlin.code.style=official
kotlin.daemon.jvm.options=-Xmx4096M
kotlin.incremental=true