Simplified packages and classes structure

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:33 +00:00
1 parent 208c587ddf
commit f6fcd21404
34 files changed
+412 -564

No files matched your search

@@ -33,13 +33,13 @@ class MainApplication : Application() {
} }
private fun checkAutoUpdate() { private fun checkAutoUpdate() {
val settingsRepository = container.settingsRepository val settingsRepo = container.settingsRepo
if (settingsRepository.isUpdateEnabled()) { if (settingsRepo.isUpdateEnabled()) {
val timeDelta = System.currentTimeMillis() - settingsRepository.getLastUpdateTime() val timeDelta = System.currentTimeMillis() - settingsRepo.getLastUpdateTime()
if (timeDelta > 172800000) { // 48 hours in ms if (timeDelta > 172800000) { // 48 hours in ms
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault()) val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
println("Started periodic data update on ${sdf.format(Date())}") println("Started periodic data update on ${sdf.format(Date())}")
container.dataRepository.updateFromWeb() container.databaseRepo.updateFromWeb()
} }
} }
} }
@@ -7,23 +7,22 @@ import android.hardware.SensorManager
import android.location.LocationManager import android.location.LocationManager
import androidx.room.Room import androidx.room.Room
import com.rtbishop.look4sat.data.DataParser import com.rtbishop.look4sat.data.DataParser
import com.rtbishop.look4sat.data.DataRepository import com.rtbishop.look4sat.data.DatabaseRepo
import com.rtbishop.look4sat.data.SatelliteRepository import com.rtbishop.look4sat.data.SatelliteRepo
import com.rtbishop.look4sat.data.SelectionRepository import com.rtbishop.look4sat.data.SelectionRepo
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDatabaseRepo
import com.rtbishop.look4sat.domain.ISatelliteRepository import com.rtbishop.look4sat.domain.ISatelliteRepo
import com.rtbishop.look4sat.domain.ISensorRepository import com.rtbishop.look4sat.domain.ISelectionRepo
import com.rtbishop.look4sat.domain.ISettingsRepository import com.rtbishop.look4sat.domain.ISensorsRepo
import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.framework.BluetoothReporter import com.rtbishop.look4sat.framework.BluetoothReporter
import com.rtbishop.look4sat.framework.NetworkReporter import com.rtbishop.look4sat.framework.NetworkReporter
import com.rtbishop.look4sat.framework.SensorRepository import com.rtbishop.look4sat.framework.SensorsRepo
import com.rtbishop.look4sat.framework.SettingsRepository import com.rtbishop.look4sat.framework.SettingsRepo
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.MIGRATION_1_2
import com.rtbishop.look4sat.framework.data.MainDatabase import com.rtbishop.look4sat.framework.data.MainDatabase
import com.rtbishop.look4sat.framework.data.local.EntriesStorage
import com.rtbishop.look4sat.framework.data.local.RadiosStorage
import com.rtbishop.look4sat.framework.data.remote.FileSource
import com.rtbishop.look4sat.framework.data.remote.NetworkSource
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -31,52 +30,48 @@ import org.osmdroid.config.Configuration
class MainContainer(private val context: Context) { class MainContainer(private val context: Context) {
val bluetoothReporter = provideBluetoothReporter() private val database = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb")
val networkReporter = provideNetworkReporter() .addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
val settingsRepository = provideSettingsRepository() private val localStorage = LocalStorage(database.storageDao())
val dataRepository = provideDataRepository(settingsRepository) val settingsRepo = provideSettingsRepo()
val sensorRepository = provideSensorRepository() val databaseRepo = provideDatabaseRepo(settingsRepo)
val satelliteRepository = provideSatelliteRepository() val sensorsRepo = provideSensorRepo()
val satelliteRepo = provideSatelliteRepo()
private fun provideBluetoothReporter(): BluetoothReporter { fun provideBluetoothReporter(): BluetoothReporter {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO)) return BluetoothReporter(btManager, CoroutineScope(Dispatchers.IO))
} }
private fun provideNetworkReporter(): NetworkReporter { fun provideNetworkReporter(): NetworkReporter {
return NetworkReporter(CoroutineScope(Dispatchers.IO)) return NetworkReporter(CoroutineScope(Dispatchers.IO))
} }
private fun provideDataRepository(settings: ISettingsRepository): IDataRepository { private fun provideDatabaseRepo(settingsRepo: ISettingsRepo): IDatabaseRepo {
val db = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb") val dataParser = DataParser(Dispatchers.Default)
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build() val dataSource = DataSource(context.contentResolver, Dispatchers.IO)
val parser = DataParser(Dispatchers.Default)
val fileSource = FileSource(context.contentResolver, Dispatchers.IO)
val entries = EntriesStorage(db.entriesDao())
val radios = RadiosStorage(db.radiosDao())
val remoteSource = NetworkSource(Dispatchers.IO)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings) return DatabaseRepo(dataParser, localStorage, dataSource, scope, settingsRepo)
} }
fun provideSelectionRepository(): SelectionRepository { fun provideSelectionRepo(): ISelectionRepo {
return SelectionRepository(Dispatchers.Default, dataRepository, settingsRepository) return SelectionRepo(Dispatchers.Default, localStorage, settingsRepo)
} }
private fun provideSensorRepository(): ISensorRepository { private fun provideSensorRepo(): ISensorsRepo {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return SensorRepository(manager, sensor) return SensorsRepo(sensorManager, sensor)
} }
private fun provideSatelliteRepository(): ISatelliteRepository { private fun provideSatelliteRepo(): ISatelliteRepo {
return SatelliteRepository(Dispatchers.Default) return SatelliteRepo(Dispatchers.Default, localStorage)
} }
private fun provideSettingsRepository(): ISettingsRepository { private fun provideSettingsRepo(): ISettingsRepo {
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE) val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
Configuration.getInstance().load(context, preferences) Configuration.getInstance().load(context, preferences)
return SettingsRepository(manager, preferences) return SettingsRepo(locationManager, preferences)
} }
} }
@@ -21,14 +21,14 @@ import android.hardware.Sensor
import android.hardware.SensorEvent import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
import android.hardware.SensorManager import android.hardware.SensorManager
import com.rtbishop.look4sat.domain.ISensorRepository import com.rtbishop.look4sat.domain.ISensorsRepo
import com.rtbishop.look4sat.domain.RAD2DEG import com.rtbishop.look4sat.domain.RAD2DEG
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlin.math.round import kotlin.math.round
class SensorRepository(private val manager: SensorManager, private val sensor: Sensor?) : class SensorsRepo(private val sensorManager: SensorManager, private val sensor: Sensor?) :
SensorEventListener, ISensorRepository { SensorEventListener, ISensorsRepo {
private val rotationMatrix = FloatArray(9) private val rotationMatrix = FloatArray(9)
private val orientationValues = FloatArray(3) private val orientationValues = FloatArray(3)
@@ -38,10 +38,10 @@ class SensorRepository(private val manager: SensorManager, private val sensor: S
override val orientation: StateFlow<Pair<Float, Float>> = _orientation override val orientation: StateFlow<Pair<Float, Float>> = _orientation
override fun enableSensor() { override fun enableSensor() {
sensor?.let { manager.registerListener(this, it, 16000) } sensor?.let { sensorManager.registerListener(this, it, 16000) }
} }
override fun disableSensor() = manager.unregisterListener(this) override fun disableSensor() = sensorManager.unregisterListener(this)
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
sensorAccuracy = accuracy sensorAccuracy = accuracy
@@ -23,7 +23,7 @@ import android.location.LocationManager
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
import androidx.core.os.CancellationSignal import androidx.core.os.CancellationSignal
import com.rtbishop.look4sat.domain.ISettingsRepository import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.model.GeoPos import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.utility.QthConverter import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round import com.rtbishop.look4sat.utility.round
@@ -31,9 +31,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.util.concurrent.Executors import java.util.concurrent.Executors
class SettingsRepository( class SettingsRepo(
private val manager: LocationManager, private val preferences: SharedPreferences private val manager: LocationManager, private val preferences: SharedPreferences
) : ISettingsRepository { ) : ISettingsRepo {
private val keyModes = "satModes" private val keyModes = "satModes"
private val keyCompass = "compass" private val keyCompass = "compass"
@@ -15,21 +15,25 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.rtbishop.look4sat.framework.data.remote package com.rtbishop.look4sat.framework.data
import android.content.ContentResolver import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import com.rtbishop.look4sat.data.IFileSource import com.rtbishop.look4sat.data.IDataSource
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.InputStream import java.io.InputStream
import java.net.URL
class FileSource( class DataSource(
private val contentResolver: ContentResolver, private val resolver: ContentResolver, private val dispatcher: CoroutineDispatcher
private val coroutineDispatcher: CoroutineDispatcher ) : IDataSource {
) : IFileSource {
override suspend fun getDataStream(uri: String): InputStream? { override suspend fun getFileStream(uri: String): InputStream? {
return withContext(coroutineDispatcher) { contentResolver.openInputStream(Uri.parse(uri)) } return withContext(dispatcher) { resolver.openInputStream(Uri.parse(uri)) }
}
override suspend fun getNetworkStream(url: String): InputStream? {
return withContext(dispatcher) { URL(url).openStream() }
} }
} }
@@ -15,38 +15,42 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.rtbishop.look4sat.framework.data.local package com.rtbishop.look4sat.framework.data
import com.rtbishop.look4sat.data.IEntriesStorage import com.rtbishop.look4sat.data.ILocalStorage
import com.rtbishop.look4sat.domain.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.framework.data.dao.EntriesDao import com.rtbishop.look4sat.model.SatRadio
import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.model.SatEntry as DomainEntry import com.rtbishop.look4sat.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.model.SatItem as DomainItem import com.rtbishop.look4sat.model.SatItem as DomainItem
import com.rtbishop.look4sat.model.SatRadio as DomainRadio
class EntriesStorage(private val entriesDao: EntriesDao) : IEntriesStorage { class LocalStorage(private val storageDao: StorageDao) : ILocalStorage {
override fun getEntriesTotal() = entriesDao.getEntriesTotal() //region # Entries region
override fun getEntriesTotal() = storageDao.getEntriesTotal()
override suspend fun getEntriesList(): List<DomainItem> { override suspend fun getEntriesList(): List<DomainItem> {
return entriesDao.getEntriesList().toDomainItems() return storageDao.getEntriesList().toDomainItems()
} }
override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> { override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> {
val selectedSatellites = mutableListOf<Satellite>() val selectedSatellites = mutableListOf<Satellite>()
ids.chunked(999).forEach { idsPart -> ids.chunked(999).forEach { idsPart ->
val entries = entriesDao.getEntriesWithIds(idsPart) val entries = storageDao.getEntriesWithIds(idsPart)
selectedSatellites.addAll(entries.map { entry -> entry.data.getSatellite() }) selectedSatellites.addAll(entries.map { entry -> entry.data.getSatellite() })
} }
return selectedSatellites return selectedSatellites
} }
override suspend fun insertEntries(entries: List<DomainEntry>) { override suspend fun insertEntries(entries: List<DomainEntry>) {
entriesDao.insertEntries(entries.toFrameworkEntries()) storageDao.insertEntries(entries.toFrameworkEntries())
} }
override suspend fun deleteEntries() = entriesDao.deleteEntries() override suspend fun deleteEntries() = storageDao.deleteEntries()
private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment) private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment)
@@ -55,4 +59,36 @@ class EntriesStorage(private val entriesDao: EntriesDao) : IEntriesStorage {
private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() } private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() }
private fun List<FrameworkItem>.toDomainItems() = this.map { item -> item.toDomain() } private fun List<FrameworkItem>.toDomainItems() = this.map { item -> item.toDomain() }
//endregion
//region # Radios region
override fun getRadiosTotal() = storageDao.getRadiosTotal()
override suspend fun getRadiosWithId(id: Int): List<SatRadio> {
return storageDao.getRadiosWithId(id).toDomainRadios()
}
override suspend fun insertRadios(radios: List<SatRadio>) {
storageDao.insertRadios(radios.toFrameworkRadios())
}
override suspend fun deleteRadios() = storageDao.deleteRadios()
private fun DomainRadio.toFramework() = FrameworkRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun FrameworkRadio.toDomain() = DomainRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
private fun List<FrameworkRadio>.toDomainRadios() = this.map { radio -> radio.toDomain() }
//endregion
} }
@@ -21,17 +21,13 @@ import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.rtbishop.look4sat.framework.data.dao.EntriesDao
import com.rtbishop.look4sat.framework.data.dao.RadiosDao
import com.rtbishop.look4sat.framework.model.SatEntry import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatRadio import com.rtbishop.look4sat.framework.model.SatRadio
@Database(entities = [SatEntry::class, SatRadio::class], version = 2) @Database(entities = [SatEntry::class, SatRadio::class], version = 2)
abstract class MainDatabase : RoomDatabase() { abstract class MainDatabase : RoomDatabase() {
abstract fun entriesDao(): EntriesDao abstract fun storageDao(): StorageDao
abstract fun radiosDao(): RadiosDao
} }
val MIGRATION_1_2 = object : Migration(1, 2) { val MIGRATION_1_2 = object : Migration(1, 2) {
@@ -15,22 +15,23 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.rtbishop.look4sat.framework.data.dao package com.rtbishop.look4sat.framework.data
import androidx.room.* import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatEntry import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatItem import com.rtbishop.look4sat.framework.model.SatItem
import com.rtbishop.look4sat.framework.model.SatRadio
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface EntriesDao { interface StorageDao {
@Query("SELECT COUNT(*) FROM entries") @Query("SELECT COUNT(*) FROM entries")
fun getEntriesTotal(): Flow<Int> fun getEntriesTotal(): Flow<Int>
// @Transaction //@Transaction
@Query("SELECT catnum, name FROM entries ORDER BY name ASC") @Query("SELECT catnum, name FROM entries ORDER BY name ASC")
suspend fun getEntriesList(): List<SatItem> suspend fun getEntriesList(): List<SatItem>
@Transaction @Transaction
@Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)") @Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)")
@@ -41,4 +42,17 @@ suspend fun getEntriesList(): List<SatItem>
@Query("DELETE FROM entries") @Query("DELETE FROM entries")
suspend fun deleteEntries() suspend fun deleteEntries()
@Query("SELECT COUNT(*) FROM radios")
fun getRadiosTotal(): Flow<Int>
@Transaction
@Query("SELECT * FROM radios WHERE catnum = :id AND isAlive = 1")
suspend fun getRadiosWithId(id: Int): List<SatRadio>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRadios(radios: List<SatRadio>)
@Query("DELETE FROM radios")
suspend fun deleteRadios()
} }
@@ -1,39 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* 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.framework.data.dao
import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatRadio
import kotlinx.coroutines.flow.Flow
@Dao
interface RadiosDao {
@Query("SELECT COUNT(*) FROM radios")
fun getRadiosTotal(): Flow<Int>
@Transaction
@Query("SELECT * FROM radios WHERE catnum = :id AND isAlive = 1")
suspend fun getRadiosWithId(id: Int): List<SatRadio>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRadios(radios: List<SatRadio>)
@Query("DELETE FROM radios")
suspend fun deleteRadios()
}
@@ -1,52 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* 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.framework.data.local
import com.rtbishop.look4sat.data.IRadiosStorage
import com.rtbishop.look4sat.framework.data.dao.RadiosDao
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.model.SatRadio as DomainRadio
class RadiosStorage(private val radiosDao: RadiosDao) : IRadiosStorage {
override fun getRadiosTotal() = radiosDao.getRadiosTotal()
override suspend fun getRadiosWithId(id: Int): List<DomainRadio> {
return radiosDao.getRadiosWithId(id).toDomainRadios()
}
override suspend fun insertRadios(radios: List<DomainRadio>) {
radiosDao.insertRadios(radios.toFrameworkRadios())
}
override suspend fun deleteRadios() = radiosDao.deleteRadios()
private fun DomainRadio.toFramework() = FrameworkRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun FrameworkRadio.toDomain() = DomainRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
private fun List<FrameworkRadio>.toDomainRadios() = this.map { radio -> radio.toDomain() }
}
@@ -1,31 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* 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.framework.data.remote
import com.rtbishop.look4sat.data.INetworkSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.net.URL
class NetworkSource(private val coroutineDispatcher: CoroutineDispatcher) : INetworkSource {
override suspend fun getDataStream(url: String): InputStream? {
return withContext(coroutineDispatcher) { URL(url).openStream() }
}
}
@@ -5,7 +5,6 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -25,8 +24,6 @@ import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
@Composable @Composable
fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier) { fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier) {
@@ -66,9 +63,3 @@ fun gotoUrl(context: Context, url: String) {
fun Modifier.onClick(onClick: () -> Unit): Modifier = composed { fun Modifier.onClick(onClick: () -> Unit): Modifier = composed {
clickable(remember { MutableInteractionSource() }, null) { onClick() } clickable(remember { MutableInteractionSource() }, null) { onClick() }
} }
class NoRippleInteractionSource : MutableInteractionSource {
override val interactions: Flow<Interaction> = emptyFlow()
override suspend fun emit(interaction: Interaction) {}
override fun tryEmit(interaction: Interaction) = true
}
@@ -45,8 +45,8 @@ fun EntriesScreen(navToPasses: () -> Unit) {
} }
} }
val unselectAll = { viewModel.updateSelection(false) } val unselectAll = { viewModel.setSelection(false) }
val selectAll = { viewModel.updateSelection(true) } val selectAll = { viewModel.setSelection(true) }
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
TopBar(setQuery = { newQuery: String -> viewModel.setQuery(newQuery) }, saveSelection = { TopBar(setQuery = { newQuery: String -> viewModel.setQuery(newQuery) }, saveSelection = {
viewModel.saveSelection() viewModel.saveSelection()
@@ -58,7 +58,7 @@ fun EntriesScreen(navToPasses: () -> Unit) {
CardLoadingIndicator() CardLoadingIndicator()
} else { } else {
EntriesCard(uiState.itemsList) { list, value -> EntriesCard(uiState.itemsList) { list, value ->
viewModel.updateSelection(list, value) viewModel.setSelection(list, value)
} }
} }
} }
@@ -2,7 +2,7 @@ package com.rtbishop.look4sat.presentation.entries
import com.rtbishop.look4sat.model.SatItem import com.rtbishop.look4sat.model.SatItem
data class EntriesScreenState( data class EntriesUiState(
val isLoading: Boolean, val isLoading: Boolean,
val itemsList: List<SatItem>, val itemsList: List<SatItem>,
val currentType: String, val currentType: String,
@@ -24,13 +24,13 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.data.SelectionRepository import com.rtbishop.look4sat.domain.ISelectionRepo
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class EntriesViewModel(private val selectionRepo: SelectionRepository) : ViewModel() { class EntriesViewModel(private val selectionRepo: ISelectionRepo) : ViewModel() {
private val defaultUiState = EntriesScreenState( private val defaultUiState = EntriesUiState(
isLoading = true, isLoading = true,
itemsList = emptyList(), itemsList = emptyList(),
currentType = selectionRepo.getCurrentType(), currentType = selectionRepo.getCurrentType(),
@@ -54,12 +54,12 @@ class EntriesViewModel(private val selectionRepo: SelectionRepository) : ViewMod
fun setQuery(query: String) = viewModelScope.launch { selectionRepo.setQuery(query) } fun setQuery(query: String) = viewModelScope.launch { selectionRepo.setQuery(query) }
fun updateSelection(selectAll: Boolean) = viewModelScope.launch { fun setSelection(selectAll: Boolean) = viewModelScope.launch {
selectionRepo.updateSelection(selectAll) selectionRepo.setSelection(selectAll)
} }
fun updateSelection(catNums: List<Int>, isSelected: Boolean) = viewModelScope.launch { fun setSelection(ids: List<Int>, isTicked: Boolean) = viewModelScope.launch {
selectionRepo.updateSelection(catNums, isSelected) selectionRepo.setSelection(ids, isTicked)
} }
fun saveSelection() = viewModelScope.launch { selectionRepo.saveSelection() } fun saveSelection() = viewModelScope.launch { selectionRepo.saveSelection() }
@@ -69,7 +69,7 @@ class EntriesViewModel(private val selectionRepo: SelectionRepository) : ViewMod
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
initializer { initializer {
val container = (this[applicationKey] as MainApplication).container val container = (this[applicationKey] as MainApplication).container
EntriesViewModel(container.provideSelectionRepository()) EntriesViewModel(container.provideSelectionRepo())
} }
} }
} }
@@ -65,7 +65,7 @@ private val labelRect = Rect()
fun MapScreen() { fun MapScreen() {
val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory) val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory)
viewModel.selectDefaultSatellite(-1) viewModel.selectDefaultSatellite(-1)
val stationPos = viewModel.stationPos.collectAsState(initial = null) val stationPos = viewModel.stationPosition.collectAsState(initial = null)
val positions = viewModel.positions.collectAsState(initial = null) val positions = viewModel.positions.collectAsState(initial = null)
val satTrack = viewModel.track.collectAsState(initial = null) val satTrack = viewModel.track.collectAsState(initial = null)
val footprint = viewModel.footprint.collectAsState(initial = null) val footprint = viewModel.footprint.collectAsState(initial = null)
@@ -23,9 +23,8 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.ISatelliteRepo
import com.rtbishop.look4sat.domain.ISatelliteRepository import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.domain.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.model.GeoPos import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPos import com.rtbishop.look4sat.model.SatPos
@@ -46,22 +45,19 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class MapViewModel( class MapViewModel(
private val dataRepository: IDataRepository, private val satelliteRepo: ISatelliteRepo, private val settingsRepo: ISettingsRepo
private val satelliteRepository: ISatelliteRepository,
private val settingsRepository: ISettingsRepository
) : ViewModel() { ) : ViewModel() {
private val stationPosNew = settingsRepository.stationPosition.value private val stationPos = settingsRepo.stationPosition.value
private val stationPosition = GeoPos(stationPosNew.latitude, stationPosNew.longitude) private var allPasses = satelliteRepo.getPasses()
private var allPasses = satelliteRepository.getPasses()
private var allSatellites = listOf<Satellite>() private var allSatellites = listOf<Satellite>()
private var dataUpdateJob: Job? = null private var dataUpdateJob: Job? = null
private var dataUpdateRate = 1000L private var dataUpdateRate = 1000L
private var selectedSatellite: Satellite? = null private var selectedSatellite: Satellite? = null
val stationPos = flow { val stationPosition = flow {
val osmLat = clipLat(stationPosition.latitude) val osmLat = clipLat(stationPos.latitude)
val osmLon = clipLon(stationPosition.longitude) val osmLon = clipLon(stationPos.longitude)
emit(GeoPos(osmLat, osmLon)) emit(GeoPos(osmLat, osmLon))
} }
@@ -92,8 +88,8 @@ class MapViewModel(
fun selectDefaultSatellite(catnum: Int) { fun selectDefaultSatellite(catnum: Int) {
viewModelScope.launch { viewModelScope.launch {
val selectedIds = settingsRepository.satelliteSelection.value val selectedIds = settingsRepo.satelliteSelection.value
dataRepository.getEntriesWithIds(selectedIds).also { satellites -> satelliteRepo.getEntriesWithIds(selectedIds).also { satellites ->
if (satellites.isNotEmpty()) { if (satellites.isNotEmpty()) {
allSatellites = satellites allSatellites = satellites
if (catnum == -1) { if (catnum == -1) {
@@ -113,12 +109,12 @@ class MapViewModel(
dataUpdateJob?.cancelAndJoin() dataUpdateJob?.cancelAndJoin()
dataUpdateJob = launch { dataUpdateJob = launch {
val dateNow = Date() val dateNow = Date()
getSatTrack(satellite, stationPosition, dateNow) getSatTrack(satellite, stationPos, dateNow)
while (isActive) { while (isActive) {
dateNow.time = System.currentTimeMillis() dateNow.time = System.currentTimeMillis()
getPositions(allSatellites, stationPosition, dateNow) getPositions(allSatellites, stationPos, dateNow)
getSatFootprint(satellite, stationPosition, dateNow) getSatFootprint(satellite, stationPos, dateNow)
getSatData(satellite, stationPosition, dateNow) getSatData(satellite, stationPos, dateNow)
delay(updateFreq) delay(updateFreq)
} }
} }
@@ -130,7 +126,7 @@ class MapViewModel(
val currentTrack = mutableListOf<GeoPos>() val currentTrack = mutableListOf<GeoPos>()
val endDate = Date(date.time + (satellite.data.orbitalPeriod * 2.4 * 60000L).toLong()) val endDate = Date(date.time + (satellite.data.orbitalPeriod * 2.4 * 60000L).toLong())
var oldLongitude = 0.0 var oldLongitude = 0.0
satelliteRepository.getTrack(satellite, pos, date.time, endDate.time).forEach { satPos -> satelliteRepo.getTrack(satellite, pos, date.time, endDate.time).forEach { satPos ->
val osmLat = clipLat(satPos.latitude.toDegrees()) val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees()) val osmLon = clipLon(satPos.longitude.toDegrees())
val currentPosition = GeoPos(osmLat, osmLon) val currentPosition = GeoPos(osmLat, osmLon)
@@ -157,7 +153,7 @@ class MapViewModel(
private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) { private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) {
val positions = mutableMapOf<Satellite, GeoPos>() val positions = mutableMapOf<Satellite, GeoPos>()
satellites.forEach { satellite -> satellites.forEach { satellite ->
val satPos = satelliteRepository.getPosition(satellite, pos, date.time) val satPos = satelliteRepo.getPosition(satellite, pos, date.time)
val osmLat = clipLat(satPos.latitude.toDegrees()) val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees()) val osmLon = clipLon(satPos.longitude.toDegrees())
positions[satellite] = GeoPos(osmLat, osmLon) positions[satellite] = GeoPos(osmLat, osmLon)
@@ -166,7 +162,7 @@ class MapViewModel(
} }
private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) { private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) {
val satPos = satelliteRepository.getPosition(satellite, pos, date.time) val satPos = satelliteRepo.getPosition(satellite, pos, date.time)
_footprint.emit(satPos) _footprint.emit(satPos)
} }
@@ -184,7 +180,7 @@ class MapViewModel(
} }
} }
} }
val satPos = satelliteRepository.getPosition(sat, pos, date.time) val satPos = satelliteRepo.getPosition(sat, pos, date.time)
val azimuth = satPos.azimuth.toDegrees() val azimuth = satPos.azimuth.toDegrees()
val elevation = satPos.elevation.toDegrees() val elevation = satPos.elevation.toDegrees()
val osmLat = clipLat(satPos.latitude.toDegrees()) val osmLat = clipLat(satPos.latitude.toDegrees())
@@ -232,11 +228,7 @@ class MapViewModel(
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
initializer { initializer {
val container = (this[applicationKey] as MainApplication).container val container = (this[applicationKey] as MainApplication).container
MapViewModel( MapViewModel(container.satelliteRepo, container.settingsRepo)
container.dataRepository,
container.satelliteRepository,
container.settingsRepository
)
} }
} }
} }
@@ -23,9 +23,8 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.ISatelliteRepo
import com.rtbishop.look4sat.domain.ISatelliteRepository import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.SatPass import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.utility.toTimerString import com.rtbishop.look4sat.utility.toTimerString
@@ -38,9 +37,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class PassesViewModel( class PassesViewModel(
private val dataRepository: IDataRepository, private val satelliteRepo: ISatelliteRepo, private val settingsRepo: ISettingsRepo
private val satelliteRepository: ISatelliteRepository,
private val settingsRepository: ISettingsRepository
) : ViewModel() { ) : ViewModel() {
private val _passes = MutableStateFlow<DataState<List<SatPass>>>(DataState.Loading) private val _passes = MutableStateFlow<DataState<List<SatPass>>>(DataState.Loading)
@@ -50,18 +47,18 @@ class PassesViewModel(
val passes: StateFlow<DataState<List<SatPass>>> = _passes val passes: StateFlow<DataState<List<SatPass>>> = _passes
val timerText: StateFlow<Triple<String, String, String>?> = _timerText val timerText: StateFlow<Triple<String, String, String>?> = _timerText
fun getHoursAhead() = settingsRepository.getHoursAhead() fun getHoursAhead() = settingsRepo.getHoursAhead()
fun getMinElevation() = settingsRepository.getMinElevation() fun getMinElevation() = settingsRepo.getMinElevation()
init { init {
viewModelScope.launch { viewModelScope.launch {
satelliteRepository.calculatedPasses.collect { passes -> satelliteRepo.calculatedPasses.collect { passes ->
passesProcessing?.cancelAndJoin() passesProcessing?.cancelAndJoin()
passesProcessing = viewModelScope.launch { passesProcessing = viewModelScope.launch {
while (isActive) { while (isActive) {
val timeNow = System.currentTimeMillis() val timeNow = System.currentTimeMillis()
val newPasses = satelliteRepository.processPasses(passes, timeNow) val newPasses = satelliteRepo.processPasses(passes, timeNow)
if (newPasses.isNotEmpty()) { if (newPasses.isNotEmpty()) {
try { try {
@@ -88,20 +85,15 @@ class PassesViewModel(
} }
} }
viewModelScope.launch { viewModelScope.launch {
settingsRepository.satelliteSelection.collect { calculatePasses() } settingsRepo.satelliteSelection.collect { calculatePasses() }
}
viewModelScope.launch {
dataRepository.updateState.collect { updateState ->
if (updateState is DataState.Success) calculatePasses()
}
} }
} }
fun shouldUseUTC() = settingsRepository.isUtcEnabled() fun shouldUseUTC() = settingsRepo.isUtcEnabled()
fun calculatePasses( fun calculatePasses(
hoursAhead: Int = 1, hoursAhead: Int = 1,
minElevation: Double = settingsRepository.getMinElevation(), minElevation: Double = settingsRepo.getMinElevation(),
timeRef: Long = System.currentTimeMillis() timeRef: Long = System.currentTimeMillis()
) { ) {
viewModelScope.launch { viewModelScope.launch {
@@ -109,12 +101,12 @@ class PassesViewModel(
// _timerText.postValue(timerDefaultText) // _timerText.postValue(timerDefaultText)
passesProcessing?.cancelAndJoin() passesProcessing?.cancelAndJoin()
// selection?.let { items -> settingsRepository.saveEntriesSelection(items) } // selection?.let { items -> settingsRepository.saveEntriesSelection(items) }
settingsRepository.setHoursAhead(hoursAhead) settingsRepo.setHoursAhead(hoursAhead)
settingsRepository.setMinElevation(minElevation) settingsRepo.setMinElevation(minElevation)
val stationPos = settingsRepository.stationPosition.value val stationPos = settingsRepo.stationPosition.value
val selectedIds = settingsRepository.satelliteSelection.value val selectedIds = settingsRepo.satelliteSelection.value
val satellites = dataRepository.getEntriesWithIds(selectedIds) val satellites = satelliteRepo.getEntriesWithIds(selectedIds)
satelliteRepository.calculatePasses( satelliteRepo.calculatePasses(
satellites, stationPos, timeRef, hoursAhead, minElevation satellites, stationPos, timeRef, hoursAhead, minElevation
) )
} }
@@ -125,11 +117,7 @@ class PassesViewModel(
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
initializer { initializer {
val container = (this[applicationKey] as MainApplication).container val container = (this[applicationKey] as MainApplication).container
PassesViewModel( PassesViewModel(container.satelliteRepo, container.settingsRepo)
container.dataRepository,
container.satelliteRepository,
container.settingsRepository
)
} }
} }
} }
@@ -28,10 +28,9 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.ISatelliteRepo
import com.rtbishop.look4sat.domain.ISatelliteRepository import com.rtbishop.look4sat.domain.ISensorsRepo
import com.rtbishop.look4sat.domain.ISensorRepository import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.domain.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.framework.BluetoothReporter import com.rtbishop.look4sat.framework.BluetoothReporter
import com.rtbishop.look4sat.framework.NetworkReporter import com.rtbishop.look4sat.framework.NetworkReporter
@@ -47,26 +46,25 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class RadarViewModel( class RadarViewModel(
private val savedStateHandle: SavedStateHandle,
private val bluetoothReporter: BluetoothReporter, private val bluetoothReporter: BluetoothReporter,
private val networkReporter: NetworkReporter, private val networkReporter: NetworkReporter,
private val savedStateHandle: SavedStateHandle, private val satelliteRepo: ISatelliteRepo,
private val dataRepository: IDataRepository, private val settingsRepo: ISettingsRepo,
private val satelliteRepository: ISatelliteRepository, private val sensorsRepo: ISensorsRepo
private val sensorRepository: ISensorRepository,
private val settingsRepository: ISettingsRepository
) : ViewModel() { ) : ViewModel() {
private val stationPos = settingsRepository.stationPosition.value private val stationPos = settingsRepo.stationPosition.value
private val magDeclination = getMagDeclination(stationPos) private val magDeclination = getMagDeclination(stationPos)
val transmitters = mutableStateOf<List<SatRadio>>(emptyList()) val transmitters = mutableStateOf<List<SatRadio>>(emptyList())
val radarData = mutableStateOf<RadarData?>(null) val radarData = mutableStateOf<RadarData?>(null)
val orientation = mutableStateOf(sensorRepository.orientation.value) val orientation = mutableStateOf(sensorsRepo.orientation.value)
init { init {
if (settingsRepository.isSensorEnabled()) { if (settingsRepo.isSensorEnabled()) {
viewModelScope.launch { viewModelScope.launch {
sensorRepository.enableSensor() sensorsRepo.enableSensor()
sensorRepository.orientation.collect { data -> sensorsRepo.orientation.collect { data ->
orientation.value = Pair(data.first + magDeclination, data.second) orientation.value = Pair(data.first + magDeclination, data.second)
} }
} }
@@ -74,24 +72,24 @@ class RadarViewModel(
} }
override fun onCleared() { override fun onCleared() {
sensorRepository.disableSensor() sensorsRepo.disableSensor()
super.onCleared() super.onCleared()
} }
fun getPass() = flow { fun getPass() = flow {
val catNum = savedStateHandle.get<Int>("catNum") ?: 0 val catNum = savedStateHandle.get<Int>("catNum") ?: 0
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
satelliteRepository.calculatedPasses.collect { passes -> satelliteRepo.calculatedPasses.collect { passes ->
val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime } val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime }
val currentPass = pass ?: passes.firstOrNull() val currentPass = pass ?: passes.firstOrNull()
currentPass?.let { satPass -> currentPass?.let { satPass ->
emit(satPass) emit(satPass)
val transmitters = dataRepository.getRadiosWithId(satPass.catNum) val transmitters = satelliteRepo.getRadiosWithId(satPass.catNum)
viewModelScope.launch { viewModelScope.launch {
while (isActive) { while (isActive) {
val timeNow = System.currentTimeMillis() val timeNow = System.currentTimeMillis()
val satPos = val satPos =
satelliteRepository.getPosition(satPass.satellite, stationPos, timeNow) satelliteRepo.getPosition(satPass.satellite, stationPos, timeNow)
sendPassData(satPass, satPos, satPass.satellite) sendPassData(satPass, satPos, satPass.satellite)
sendPassDataBT(satPos) sendPassDataBT(satPos)
processRadios(transmitters, satPass.satellite, timeNow) processRadios(transmitters, satPass.satellite, timeNow)
@@ -102,27 +100,27 @@ class RadarViewModel(
} }
} }
fun getUseCompass(): Boolean = settingsRepository.isSensorEnabled() fun getUseCompass(): Boolean = settingsRepo.isSensorEnabled()
fun getShowSweep(): Boolean = settingsRepository.isSweepEnabled() fun getShowSweep(): Boolean = settingsRepo.isSweepEnabled()
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float { private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.latitude.toFloat() val latitude = geoPos.latitude.toFloat()
val longitude = geoPos.longitude.toFloat() val longitude = geoPos.longitude.toFloat()
return GeomagneticField(latitude, longitude, 0f, time).declination return GeomagneticField(latitude, longitude, geoPos.altitude.toFloat(), time).declination
} }
private fun sendPassData(satPass: SatPass, satPos: SatPos, satellite: Satellite) { private fun sendPassData(satPass: SatPass, satPos: SatPos, satellite: Satellite) {
viewModelScope.launch { viewModelScope.launch {
var track: List<SatPos> = emptyList() var track: List<SatPos> = emptyList()
if (!satPass.isDeepSpace) { if (!satPass.isDeepSpace) {
track = satelliteRepository.getTrack( track = satelliteRepo.getTrack(
satellite, stationPos, satPass.aosTime, satPass.losTime satellite, stationPos, satPass.aosTime, satPass.losTime
) )
} }
if (settingsRepository.getRotatorEnabled()) { if (settingsRepo.getRotatorEnabled()) {
val server = settingsRepository.getRotatorServer() val server = settingsRepo.getRotatorServer()
val port = settingsRepository.getRotatorPort().toInt() val port = settingsRepo.getRotatorPort().toInt()
val azimuth = satPos.azimuth.toDegrees().round(1) val azimuth = satPos.azimuth.toDegrees().round(1)
val elevation = satPos.elevation.toDegrees().round(1) val elevation = satPos.elevation.toDegrees().round(1)
networkReporter.reportRotation(server, port, azimuth, elevation) networkReporter.reportRotation(server, port, azimuth, elevation)
@@ -133,10 +131,10 @@ class RadarViewModel(
private fun sendPassDataBT(satPos: SatPos) { private fun sendPassDataBT(satPos: SatPos) {
viewModelScope.launch { viewModelScope.launch {
if (settingsRepository.getBTEnabled()) { if (settingsRepo.getBTEnabled()) {
val btDevice = settingsRepository.getBTDeviceAddr() val btDevice = settingsRepo.getBTDeviceAddr()
if (bluetoothReporter.isConnected()) { if (bluetoothReporter.isConnected()) {
val format = settingsRepository.getBTFormat() val format = settingsRepo.getBTFormat()
val azimuth = satPos.azimuth.toDegrees().round(0).toInt() val azimuth = satPos.azimuth.toDegrees().round(0).toInt()
val elevation = satPos.elevation.toDegrees().round(0).toInt() val elevation = satPos.elevation.toDegrees().round(0).toInt()
bluetoothReporter.reportRotation(format, azimuth, elevation) bluetoothReporter.reportRotation(format, azimuth, elevation)
@@ -151,7 +149,7 @@ class RadarViewModel(
private fun processRadios(radios: List<SatRadio>, satellite: Satellite, time: Long) { private fun processRadios(radios: List<SatRadio>, satellite: Satellite, time: Long) {
viewModelScope.launch { viewModelScope.launch {
delay(125) delay(125)
val list = satelliteRepository.processRadios(satellite, stationPos, radios, time) val list = satelliteRepo.processRadios(satellite, stationPos, radios, time)
transmitters.value = list transmitters.value = list
} }
} }
@@ -160,16 +158,14 @@ class RadarViewModel(
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
initializer { initializer {
val savedStateHandle = createSavedStateHandle()
val container = (this[applicationKey] as MainApplication).container val container = (this[applicationKey] as MainApplication).container
RadarViewModel( RadarViewModel(
container.bluetoothReporter, createSavedStateHandle(),
container.networkReporter, container.provideBluetoothReporter(),
savedStateHandle, container.provideNetworkReporter(),
container.dataRepository, container.satelliteRepo,
container.satelliteRepository, container.settingsRepo,
container.sensorRepository, container.sensorsRepo
container.settingsRepository
) )
} }
} }
@@ -25,18 +25,18 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDatabaseRepo
import com.rtbishop.look4sat.domain.ISettingsRepository import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.model.DataState import com.rtbishop.look4sat.model.DataState
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SettingsViewModel( class SettingsViewModel(
private val dataRepository: IDataRepository, private val settingsRepository: ISettingsRepository private val databaseRepo: IDatabaseRepo, private val settingsRepo: ISettingsRepo
) : ViewModel() { ) : ViewModel() {
private val defaultLocationSettings = LocationSettings(false, private val defaultLocationSettings = LocationSettings(false,
settingsRepository.stationPosition.value, settingsRepo.stationPosition.value,
0, 0,
{ setGpsPosition() }, { setGpsPosition() },
{ latitude, longitude -> setGeoPosition(latitude, longitude) }, { latitude, longitude -> setGeoPosition(latitude, longitude) },
@@ -45,18 +45,18 @@ class SettingsViewModel(
val locationSettings = mutableStateOf(defaultLocationSettings) val locationSettings = mutableStateOf(defaultLocationSettings)
private val defaultDataSettings = DataSettings(false, private val defaultDataSettings = DataSettings(false,
settingsRepository.getLastUpdateTime(), settingsRepo.getLastUpdateTime(),
0, 0,
0, 0,
{ dataRepository.updateFromWeb() }, { databaseRepo.updateFromWeb() },
{ dataRepository.updateFromFile(it) }, { databaseRepo.updateFromFile(it) },
{ dataRepository.clearAllData() }) { databaseRepo.clearAllData() })
val dataSettings = mutableStateOf(defaultDataSettings) val dataSettings = mutableStateOf(defaultDataSettings)
private val defaultOtherSettings = OtherSettings(settingsRepository.isUtcEnabled(), private val defaultOtherSettings = OtherSettings(settingsRepo.isUtcEnabled(),
settingsRepository.isUpdateEnabled(), settingsRepo.isUpdateEnabled(),
settingsRepository.isSweepEnabled(), settingsRepo.isSweepEnabled(),
settingsRepository.isSensorEnabled(), settingsRepo.isSensorEnabled(),
{ setUtc(it) }, { setUtc(it) },
{ setUpdate(it) }, { setUpdate(it) },
{ setSweep(it) }, { setSweep(it) },
@@ -65,23 +65,23 @@ class SettingsViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
settingsRepository.stationPosition.collect { stationPos -> settingsRepo.stationPosition.collect { stationPos ->
locationSettings.value = locationSettings.value.copy( locationSettings.value = locationSettings.value.copy(
isUpdating = false, stationPos = stationPos isUpdating = false, stationPos = stationPos
) )
} }
} }
viewModelScope.launch { viewModelScope.launch {
dataRepository.updateState.collect { databaseRepo.updateState.collect {
val isUpdating = it is DataState.Loading val isUpdating = it is DataState.Loading
dataSettings.value = dataSettings.value.copy( dataSettings.value = dataSettings.value.copy(
isUpdating = isUpdating, lastUpdated = settingsRepository.getLastUpdateTime() isUpdating = isUpdating, lastUpdated = settingsRepo.getLastUpdateTime()
) )
} }
} }
viewModelScope.launch { viewModelScope.launch {
combine( combine(
dataRepository.getEntriesTotal(), dataRepository.getRadiosTotal() databaseRepo.getEntriesTotal(), databaseRepo.getRadiosTotal()
) { sats, radios -> ) { sats, radios ->
dataSettings.value = dataSettings.value.copy(satsTotal = sats, radiosTotal = radios) dataSettings.value = dataSettings.value.copy(satsTotal = sats, radiosTotal = radios)
}.collect {} }.collect {}
@@ -89,7 +89,7 @@ class SettingsViewModel(
} }
private fun setGpsPosition() { private fun setGpsPosition() {
if (settingsRepository.setGpsPosition()) { if (settingsRepo.setGpsPosition()) {
val messageResId = R.string.location_success val messageResId = R.string.location_success
locationSettings.value = locationSettings.value =
locationSettings.value.copy(isUpdating = true, messageResId = messageResId) locationSettings.value.copy(isUpdating = true, messageResId = messageResId)
@@ -100,7 +100,7 @@ class SettingsViewModel(
} }
private fun setGeoPosition(latitude: Double, longitude: Double) { private fun setGeoPosition(latitude: Double, longitude: Double) {
if (settingsRepository.setGeoPosition(latitude, longitude)) { if (settingsRepo.setGeoPosition(latitude, longitude)) {
val messageResId = R.string.location_success val messageResId = R.string.location_success
locationSettings.value = locationSettings.value.copy(messageResId = messageResId) locationSettings.value = locationSettings.value.copy(messageResId = messageResId)
} else { } else {
@@ -110,7 +110,7 @@ class SettingsViewModel(
} }
private fun setQthPosition(locator: String) { private fun setQthPosition(locator: String) {
if (settingsRepository.setQthPosition(locator)) { if (settingsRepo.setQthPosition(locator)) {
val messageResId = R.string.location_success val messageResId = R.string.location_success
locationSettings.value = locationSettings.value.copy(messageResId = messageResId) locationSettings.value = locationSettings.value.copy(messageResId = messageResId)
} else { } else {
@@ -124,22 +124,22 @@ class SettingsViewModel(
} }
private fun setUtc(value: Boolean) { private fun setUtc(value: Boolean) {
settingsRepository.setUtcState(value) settingsRepo.setUtcState(value)
otherSettings.value = otherSettings.value.copy(getUtc = value) otherSettings.value = otherSettings.value.copy(getUtc = value)
} }
private fun setUpdate(value: Boolean) { private fun setUpdate(value: Boolean) {
settingsRepository.setUpdateState(value) settingsRepo.setUpdateState(value)
otherSettings.value = otherSettings.value.copy(getUpdate = value) otherSettings.value = otherSettings.value.copy(getUpdate = value)
} }
private fun setSweep(value: Boolean) { private fun setSweep(value: Boolean) {
settingsRepository.setSweepState(value) settingsRepo.setSweepState(value)
otherSettings.value = otherSettings.value.copy(getSweep = value) otherSettings.value = otherSettings.value.copy(getSweep = value)
} }
private fun setSensor(value: Boolean) { private fun setSensor(value: Boolean) {
settingsRepository.setSensorState(value) settingsRepo.setSensorState(value)
otherSettings.value = otherSettings.value.copy(getSensor = value) otherSettings.value = otherSettings.value.copy(getSensor = value)
} }
@@ -163,7 +163,7 @@ class SettingsViewModel(
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
initializer { initializer {
val container = (this[applicationKey] as MainApplication).container val container = (this[applicationKey] as MainApplication).container
SettingsViewModel(container.dataRepository, container.settingsRepository) SettingsViewModel(container.databaseRepo, container.settingsRepo)
} }
} }
} }
@@ -17,11 +17,10 @@
*/ */
package com.rtbishop.look4sat.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDatabaseRepo
import com.rtbishop.look4sat.domain.ISettingsRepository import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.SatEntry import com.rtbishop.look4sat.model.SatEntry
import com.rtbishop.look4sat.model.SatItem
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -32,15 +31,13 @@ import kotlinx.coroutines.launch
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
class DataRepository( class DatabaseRepo(
private val dataParser: DataParser, private val dataParser: DataParser,
private val fileSource: IFileSource, private val localStorage: ILocalStorage,
private val entrySource: IEntriesStorage, private val dataSource: IDataSource,
private val radioSource: IRadiosStorage,
private val remoteSource: INetworkSource,
private val repositoryScope: CoroutineScope, private val repositoryScope: CoroutineScope,
private val settingsRepository: ISettingsRepository private val settingsRepository: ISettingsRepo
) : IDataRepository { ) : IDatabaseRepo {
private val exceptionHandler = CoroutineExceptionHandler { _, exception -> private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
_updateState.value = DataState.Exception(exception.message) _updateState.value = DataState.Exception(exception.message)
@@ -49,27 +46,16 @@ class DataRepository(
private val _updateState = MutableStateFlow<DataState<Long>>(DataState.Handled) private val _updateState = MutableStateFlow<DataState<Long>>(DataState.Handled)
override val updateState: StateFlow<DataState<Long>> = _updateState override val updateState: StateFlow<DataState<Long>> = _updateState
override fun getEntriesTotal() = entrySource.getEntriesTotal() override fun getEntriesTotal() = localStorage.getEntriesTotal()
override fun getRadiosTotal() = radioSource.getRadiosTotal() override fun getRadiosTotal() = localStorage.getRadiosTotal()
override fun getSatelliteTypes() = remoteSource.satelliteSourcesMap.keys.sorted()
override suspend fun getEntriesList(): List<SatItem> {
val selectedIds = settingsRepository.satelliteSelection.value
return entrySource.getEntriesList().map { it.copy(isSelected = it.catnum in selectedIds) }
}
override suspend fun getEntriesWithIds(ids: List<Int>) = entrySource.getEntriesWithIds(ids)
override suspend fun getRadiosWithId(id: Int) = radioSource.getRadiosWithId(id)
override fun updateFromFile(uri: String) { override fun updateFromFile(uri: String) {
repositoryScope.launch(exceptionHandler) { repositoryScope.launch(exceptionHandler) {
_updateState.value = DataState.Loading _updateState.value = DataState.Loading
fileSource.getDataStream(uri)?.let { stream -> dataSource.getFileStream(uri)?.let { stream ->
delay(updateStateDelay) delay(updateStateDelay)
entrySource.insertEntries(importSatellites(stream)) localStorage.insertEntries(importSatellites(stream))
} }
setUpdateSuccessful() setUpdateSuccessful()
} }
@@ -79,8 +65,8 @@ class DataRepository(
_updateState.value = DataState.Loading _updateState.value = DataState.Loading
repositoryScope.launch(exceptionHandler) { repositoryScope.launch(exceptionHandler) {
val importedEntries = mutableListOf<SatEntry>() val importedEntries = mutableListOf<SatEntry>()
val sourcesMap = remoteSource.satelliteSourcesMap val sourcesMap = settingsRepository.satelliteSourcesMap
val jobsMap = sourcesMap.mapValues { async { remoteSource.getDataStream(it.value) } } val jobsMap = sourcesMap.mapValues { async { dataSource.getNetworkStream(it.value) } }
jobsMap.mapValues { job -> job.value.await() }.forEach { entry -> jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream -> entry.value?.let { stream ->
when (val type = entry.key) { when (val type = entry.key) {
@@ -112,12 +98,12 @@ class DataRepository(
} }
} }
} }
entrySource.insertEntries(importedEntries) localStorage.insertEntries(importedEntries)
setUpdateSuccessful() setUpdateSuccessful()
} }
repositoryScope.launch(exceptionHandler) { repositoryScope.launch(exceptionHandler) {
remoteSource.getDataStream(remoteSource.radioSourceUrl)?.let { stream -> dataSource.getNetworkStream(settingsRepository.radioSourceUrl)?.let { stream ->
radioSource.insertRadios(dataParser.parseJSONStream(stream)) localStorage.insertRadios(dataParser.parseJSONStream(stream))
} }
} }
} }
@@ -130,8 +116,8 @@ class DataRepository(
repositoryScope.launch { repositoryScope.launch {
_updateState.value = DataState.Loading _updateState.value = DataState.Loading
delay(updateStateDelay) delay(updateStateDelay)
entrySource.deleteEntries() localStorage.deleteEntries()
radioSource.deleteRadios() localStorage.deleteRadios()
setUpdateSuccessful(0L) setUpdateSuccessful(0L)
} }
} }
@@ -19,7 +19,9 @@ package com.rtbishop.look4sat.data
import java.io.InputStream import java.io.InputStream
interface IFileSource { interface IDataSource {
suspend fun getDataStream(uri: String): InputStream? suspend fun getFileStream(uri: String): InputStream?
suspend fun getNetworkStream(url: String): InputStream?
} }
@@ -20,9 +20,10 @@ package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.model.SatEntry import com.rtbishop.look4sat.model.SatEntry
import com.rtbishop.look4sat.model.SatItem import com.rtbishop.look4sat.model.SatItem
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface IEntriesStorage { interface ILocalStorage {
fun getEntriesTotal(): Flow<Int> fun getEntriesTotal(): Flow<Int>
@@ -33,4 +34,12 @@ interface IEntriesStorage {
suspend fun insertEntries(entries: List<SatEntry>) suspend fun insertEntries(entries: List<SatEntry>)
suspend fun deleteEntries() suspend fun deleteEntries()
fun getRadiosTotal(): Flow<Int>
suspend fun getRadiosWithId(id: Int): List<SatRadio>
suspend fun insertRadios(radios: List<SatRadio>)
suspend fun deleteRadios()
} }
@@ -1,32 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* 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.data
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.Flow
interface IRadiosStorage {
fun getRadiosTotal(): Flow<Int>
suspend fun getRadiosWithId(id: Int): List<SatRadio>
suspend fun insertRadios(radios: List<SatRadio>)
suspend fun deleteRadios()
}
@@ -17,7 +17,7 @@
*/ */
package com.rtbishop.look4sat.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.ISatelliteRepository import com.rtbishop.look4sat.domain.ISatelliteRepo
import com.rtbishop.look4sat.domain.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.model.GeoPos import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPass import com.rtbishop.look4sat.model.SatPass
@@ -30,7 +30,10 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class SatelliteRepository(private val dispatcher: CoroutineDispatcher) : ISatelliteRepository { class SatelliteRepo(
private val dispatcher: CoroutineDispatcher,
private val localStorage: ILocalStorage
) : ISatelliteRepo {
private var passes = listOf<SatPass>() private var passes = listOf<SatPass>()
private val _calculatedPasses = MutableSharedFlow<List<SatPass>>(replay = 1) private val _calculatedPasses = MutableSharedFlow<List<SatPass>>(replay = 1)
@@ -38,6 +41,10 @@ class SatelliteRepository(private val dispatcher: CoroutineDispatcher) : ISatell
override fun getPasses(): List<SatPass> = passes override fun getPasses(): List<SatPass> = passes
override suspend fun getEntriesWithIds(ids: List<Int>) = localStorage.getEntriesWithIds(ids)
override suspend fun getRadiosWithId(id: Int) = localStorage.getRadiosWithId(id)
override suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos { override suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos {
return withContext(dispatcher) { sat.getPosition(pos, time) } return withContext(dispatcher) { sat.getPosition(pos, time) }
} }
@@ -1,7 +1,7 @@
package com.rtbishop.look4sat.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.ISelectionRepo
import com.rtbishop.look4sat.domain.ISettingsRepository import com.rtbishop.look4sat.domain.ISettingsRepo
import com.rtbishop.look4sat.model.SatItem import com.rtbishop.look4sat.model.SatItem
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -12,11 +12,11 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class SelectionRepository( class SelectionRepo(
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
private val dataRepository: IDataRepository, private val localStorage: ILocalStorage,
private val settingsRepository: ISettingsRepository private val settingsRepo: ISettingsRepo
) { ) : ISelectionRepo {
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList()) private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
private val currentType = MutableStateFlow("All") private val currentType = MutableStateFlow("All")
private val currentQuery = MutableStateFlow("") private val currentQuery = MutableStateFlow("")
@@ -27,37 +27,44 @@ class SelectionRepository(
itemsWithType.map { items -> items.filterByQuery(query) } itemsWithType.map { items -> items.filterByQuery(query) }
} }
fun getCurrentType() = currentType.value override fun getCurrentType() = currentType.value
fun getTypesList() = dataRepository.getSatelliteTypes() override fun getTypesList() = settingsRepo.satelliteSourcesMap.keys.sorted()
suspend fun getEntriesFlow() = withContext(dispatcher) { override suspend fun getEntriesFlow() = withContext(dispatcher) {
currentItems.value = dataRepository.getEntriesList() val selectedIds = settingsRepo.satelliteSelection.value
currentItems.value = localStorage.getEntriesList().map { item ->
item.copy(isSelected = item.catnum in selectedIds)
}
return@withContext itemsWithQuery return@withContext itemsWithQuery
} }
suspend fun setType(type: String) = withContext(dispatcher) { currentType.value = type } override suspend fun setType(type: String) = withContext(dispatcher) {
currentType.value = type
suspend fun setQuery(query: String) = withContext(dispatcher) { currentQuery.value = query }
suspend fun updateSelection(selectAll: Boolean) = withContext(dispatcher) {
updateSelection(itemsWithQuery.first().map { item -> item.catnum }, selectAll)
} }
suspend fun updateSelection(catNums: List<Int>, isSelected: Boolean) = withContext(dispatcher) { override suspend fun setQuery(query: String) = withContext(dispatcher) {
currentQuery.value = query
}
override suspend fun setSelection(selectAll: Boolean) = withContext(dispatcher) {
setSelection(itemsWithQuery.first().map { item -> item.catnum }, selectAll)
}
override suspend fun setSelection(ids: List<Int>, isTicked: Boolean) = withContext(dispatcher) {
currentItems.value = currentItems.value.map { item -> currentItems.value = currentItems.value.map { item ->
if (item.catnum in catNums) item.copy(isSelected = isSelected) else item if (item.catnum in ids) item.copy(isSelected = isTicked) else item
} }
} }
suspend fun saveSelection() = withContext(dispatcher) { override suspend fun saveSelection() = withContext(dispatcher) {
val currentSelection = currentItems.value.filter { it.isSelected }.map { it.catnum } val currentSelection = currentItems.value.filter { it.isSelected }.map { it.catnum }
settingsRepository.saveEntriesSelection(currentSelection) settingsRepo.saveEntriesSelection(currentSelection)
} }
private suspend fun List<SatItem>.filterByType(type: String) = withContext(dispatcher) { private suspend fun List<SatItem>.filterByType(type: String) = withContext(dispatcher) {
if (type == "All") return@withContext this@filterByType if (type == "All") return@withContext this@filterByType
val catnums = settingsRepository.loadSatType(type) val catnums = settingsRepo.loadSatType(type)
if (catnums.isEmpty()) return@withContext this@filterByType if (catnums.isEmpty()) return@withContext this@filterByType
return@withContext this@filterByType.filter { item -> item.catnum in catnums } return@withContext this@filterByType.filter { item -> item.catnum in catnums }
} }
@@ -18,12 +18,10 @@
package com.rtbishop.look4sat.domain package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.SatItem
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface IDataRepository { interface IDatabaseRepo {
val updateState: StateFlow<DataState<Long>> val updateState: StateFlow<DataState<Long>>
@@ -31,14 +29,6 @@ interface IDataRepository {
fun getRadiosTotal(): Flow<Int> fun getRadiosTotal(): Flow<Int>
fun getSatelliteTypes(): List<String>
suspend fun getEntriesList(): List<SatItem>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
suspend fun getRadiosWithId(id: Int): List<SatRadio>
fun updateFromFile(uri: String) fun updateFromFile(uri: String)
fun updateFromWeb() fun updateFromWeb()
@@ -6,12 +6,16 @@ import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.model.SatRadio import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
interface ISatelliteRepository { interface ISatelliteRepo {
val calculatedPasses: SharedFlow<List<SatPass>> val calculatedPasses: SharedFlow<List<SatPass>>
fun getPasses(): List<SatPass> fun getPasses(): List<SatPass>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
suspend fun getRadiosWithId(id: Int): List<SatRadio>
suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos
suspend fun getTrack(sat: Satellite, pos: GeoPos, start: Long, end: Long): List<SatPos> suspend fun getTrack(sat: Satellite, pos: GeoPos, start: Long, end: Long): List<SatPos>
@@ -0,0 +1,23 @@
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.SatItem
import kotlinx.coroutines.flow.Flow
interface ISelectionRepo {
fun getCurrentType(): String
fun getTypesList(): List<String>
suspend fun getEntriesFlow(): Flow<List<SatItem>>
suspend fun setType(type: String)
suspend fun setQuery(query: String)
suspend fun setSelection(selectAll: Boolean)
suspend fun setSelection(ids: List<Int>, isTicked: Boolean)
suspend fun saveSelection()
}
@@ -2,7 +2,7 @@ package com.rtbishop.look4sat.domain
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface ISensorRepository { interface ISensorsRepo {
val orientation: StateFlow<Pair<Float, Float>> val orientation: StateFlow<Pair<Float, Float>>
@@ -15,13 +15,15 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.rtbishop.look4sat.data package com.rtbishop.look4sat.domain
import java.io.InputStream import com.rtbishop.look4sat.model.GeoPos
import kotlinx.coroutines.flow.StateFlow
interface INetworkSource { interface ISettingsRepo {
val radioSourceUrl: String get() = "https://db.satnogs.org/api/transmitters/?format=json" val radioSourceUrl: String get() = "https://db.satnogs.org/api/transmitters/?format=json"
val satelliteSourcesMap: Map<String, String> val satelliteSourcesMap: Map<String, String>
get() = mapOf( get() = mapOf(
"All" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=active&FORMAT=csv", "All" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
@@ -52,5 +54,79 @@ interface INetworkSource {
"X-Comm" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv" "X-Comm" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv"
) )
suspend fun getDataStream(url: String): InputStream? val stationPosition: StateFlow<GeoPos>
val satelliteSelection: StateFlow<List<Int>>
fun setGpsPosition(): Boolean
fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double = 0.0): Boolean
fun setQthPosition(locator: String): Boolean
fun getHoursAhead(): Int
fun setHoursAhead(hoursAhead: Int)
fun getMinElevation(): Double
fun setMinElevation(minElevation: Double)
fun isUtcEnabled(): Boolean
fun setUtcState(value: Boolean)
fun getLastUpdateTime(): Long
fun setLastUpdateTime(updateTime: Long)
fun isUpdateEnabled(): Boolean
fun setUpdateState(value: Boolean)
fun isSensorEnabled(): Boolean
fun setSensorState(value: Boolean)
fun isSweepEnabled(): Boolean
fun setSweepState(value: Boolean)
fun saveModesSelection(modes: List<String>)
fun loadModesSelection(): List<String>
fun saveEntriesSelection(catnums: List<Int>)
fun saveSatType(type: String, catnums: List<Int>)
fun loadSatType(type: String): List<Int>
fun getRotatorEnabled(): Boolean
fun setRotatorEnabled(value: Boolean)
fun getRotatorServer(): String
fun setRotatorServer(value: String)
fun getRotatorPort(): String
fun setRotatorPort(value: String)
fun getBTEnabled(): Boolean
fun setBTEnabled(value: Boolean)
fun getBTDeviceAddr(): String
fun setBTDeviceAddr(value: String)
fun getBTDeviceName(): String
fun setBTDeviceName(value: String)
fun getBTFormat(): String
fun setBTFormat(value: String)
} }
@@ -1,100 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* 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.domain
import com.rtbishop.look4sat.model.GeoPos
import kotlinx.coroutines.flow.StateFlow
interface ISettingsRepository {
val stationPosition: StateFlow<GeoPos>
val satelliteSelection: StateFlow<List<Int>>
fun setGpsPosition(): Boolean
fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double = 0.0): Boolean
fun setQthPosition(locator: String): Boolean
fun getHoursAhead(): Int
fun setHoursAhead(hoursAhead: Int)
fun getMinElevation(): Double
fun setMinElevation(minElevation: Double)
fun isUtcEnabled(): Boolean
fun setUtcState(value: Boolean)
fun getLastUpdateTime(): Long
fun setLastUpdateTime(updateTime: Long)
fun isUpdateEnabled(): Boolean
fun setUpdateState(value: Boolean)
fun isSensorEnabled(): Boolean
fun setSensorState(value: Boolean)
fun isSweepEnabled(): Boolean
fun setSweepState(value: Boolean)
fun saveModesSelection(modes: List<String>)
fun loadModesSelection(): List<String>
fun saveEntriesSelection(catnums: List<Int>)
fun saveSatType(type: String, catnums: List<Int>)
fun loadSatType(type: String): List<Int>
fun getRotatorEnabled(): Boolean
fun setRotatorEnabled(value: Boolean)
fun getRotatorServer(): String
fun setRotatorServer(value: String)
fun getRotatorPort(): String
fun setRotatorPort(value: String)
fun getBTEnabled(): Boolean
fun setBTEnabled(value: Boolean)
fun getBTDeviceAddr(): String
fun setBTDeviceAddr(value: String)
fun getBTDeviceName(): String
fun setBTDeviceName(value: String)
fun getBTFormat(): String
fun setBTFormat(value: String)
}
@@ -72,17 +72,3 @@ fun String.isValidPort(): Boolean {
val port = "([1-9]|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])" val port = "([1-9]|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])"
return this.matches(port.toRegex()) && this.toInt() in 1024..65535 return this.matches(port.toRegex()) && this.toInt() in 1024..65535
} }
//fun ping(hostname: String, port: Int): Int {
// val start = System.currentTimeMillis()
// val socket = Socket()
// try {
// socket.connect(InetSocketAddress(hostname, port), 5000)
// socket.close()
// } catch (exception: Exception) {
// exception.printStackTrace()
// println("Failed to ping: $hostname")
// return Int.MAX_VALUE
// }
// return (System.currentTimeMillis() - start).toInt()
//}