mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Added basic convention plugins setup for modularization
This commit is contained in:
1 parent
de99e09f5e
commit
4d88214c1d
5675 files changed
+10339
-9969
No files matched your search
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.convention.androidLibraryPlugin)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.rtbishop.look4sat.core.data"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatEntry
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.model.SatItem
|
||||
|
||||
@Dao
|
||||
interface Look4SatDao {
|
||||
|
||||
@Query("SELECT COUNT(*) FROM entries")
|
||||
suspend fun getEntriesTotal(): Int
|
||||
|
||||
@Query("SELECT catnum, name, 0 as isSelected FROM entries ORDER BY name ASC")
|
||||
suspend fun getEntriesList(): List<SatItem>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)")
|
||||
suspend fun getEntriesWithIds(selectedIds: List<Int>): List<SatEntry>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertEntries(entries: List<SatEntry>)
|
||||
|
||||
@Query("DELETE FROM entries")
|
||||
suspend fun deleteEntries()
|
||||
|
||||
@Query("SELECT catnum FROM radios WHERE downlinkMode IN (:modes)")
|
||||
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM radios")
|
||||
suspend fun getRadiosTotal(): 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()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatEntry
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatRadio
|
||||
|
||||
@Database(entities = [SatEntry::class, SatRadio::class], version = 1, exportSchema = false)
|
||||
abstract class Look4SatDb : RoomDatabase() {
|
||||
abstract fun look4SatDao(): Look4SatDao
|
||||
}
|
||||
|
||||
//val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
// override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// database.execSQL("CREATE TABLE entries_backup (name TEXT NOT NULL, epoch REAL NOT NULL, meanmo REAL NOT NULL, eccn REAL NOT NULL, incl REAL NOT NULL, raan REAL NOT NULL, argper REAL NOT NULL, meanan REAL NOT NULL, catnum INTEGER NOT NULL, bstar REAL NOT NULL, xincl REAL NOT NULL, xnodeo REAL NOT NULL, omegao REAL NOT NULL, xmo REAL NOT NULL, xno REAL NOT NULL, orbitalPeriod REAL NOT NULL, isDeepSpace INTEGER NOT NULL, comment TEXT, PRIMARY KEY(catnum))")
|
||||
// database.execSQL("INSERT INTO entries_backup (name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, orbitalPeriod, isDeepSpace, comment) SELECT name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, 1440 / meanmo, 1440 / meanmo >= 225.0, comment FROM entries")
|
||||
// database.execSQL("DROP TABLE entries")
|
||||
// database.execSQL("ALTER TABLE entries_backup RENAME TO entries")
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.database.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
|
||||
@Entity(tableName = "entries", primaryKeys = ["catnum"])
|
||||
data class SatEntry(
|
||||
val name: String,
|
||||
val epoch: Double,
|
||||
val meanmo: Double,
|
||||
val eccn: Double,
|
||||
val incl: Double,
|
||||
val raan: Double,
|
||||
val argper: Double,
|
||||
val meanan: Double,
|
||||
val catnum: Int,
|
||||
val bstar: Double
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.database.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "radios")
|
||||
data class SatRadio(
|
||||
@PrimaryKey val uuid: String,
|
||||
val info: String,
|
||||
val isAlive: Boolean,
|
||||
var downlinkLow: Long?,
|
||||
var downlinkHigh: Long?,
|
||||
val downlinkMode: String?,
|
||||
var uplinkLow: Long?,
|
||||
var uplinkHigh: Long?,
|
||||
val uplinkMode: String?,
|
||||
val isInverted: Boolean,
|
||||
val catnum: Int?
|
||||
)
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.framework
|
||||
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.util.Log
|
||||
import com.rtbishop.look4sat.core.domain.repository.BtService
|
||||
import com.rtbishop.look4sat.core.domain.repository.IReporterRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.WithoutExtParams
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.OutputStream
|
||||
import java.util.UUID
|
||||
import kotlin.math.abs
|
||||
|
||||
data class DeviceConnection(
|
||||
var socket: BluetoothSocket? = null,
|
||||
var outputStream: OutputStream? = null,
|
||||
var connected: Boolean = false,
|
||||
var connecting: Boolean = false,
|
||||
var connectionJob: Job? = null
|
||||
)
|
||||
|
||||
class BluetoothReporter(
|
||||
private val bluetoothManager: BluetoothManager,
|
||||
private val reporterScope: CoroutineScope
|
||||
) : IReporterRepo<WithoutExtParams> {
|
||||
|
||||
private val tag = "BTReporter"
|
||||
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
private val serviceToDevice = mutableMapOf<BtService, String>()
|
||||
private val deviceConnections = mutableMapOf<String, DeviceConnection>()
|
||||
private val writeMutex = Mutex()
|
||||
|
||||
override fun isConnected(service: BtService): Boolean {
|
||||
val deviceId = serviceToDevice[service] ?: return false
|
||||
return deviceConnections[deviceId]?.connected == true
|
||||
}
|
||||
|
||||
override fun isConnecting(service: BtService): Boolean {
|
||||
val deviceId = serviceToDevice[service] ?: return false
|
||||
return deviceConnections[deviceId]?.connecting == true
|
||||
}
|
||||
|
||||
override fun connect(service: BtService, deviceId: String) {
|
||||
serviceToDevice[service] = deviceId
|
||||
val connection = deviceConnections.getOrPut(deviceId) {
|
||||
DeviceConnection()
|
||||
}
|
||||
if (connection.connected || connection.connecting) return
|
||||
connection.connectionJob = reporterScope.launch {
|
||||
try {
|
||||
connection.connecting = true
|
||||
val device = bluetoothManager.adapter.getRemoteDevice(deviceId)
|
||||
val socket = device.createInsecureRfcommSocketToServiceRecord(sppid)
|
||||
socket.connect()
|
||||
connection.socket = socket
|
||||
connection.outputStream = socket.outputStream
|
||||
connection.connected = true
|
||||
Log.i(tag, "$tag: Connected to $deviceId")
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "$tag: ${e.message}")
|
||||
connection.connected = false
|
||||
} finally {
|
||||
connection.connecting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun write(service: BtService, buffer: String) {
|
||||
val deviceId = serviceToDevice[service] ?: return
|
||||
val connection = deviceConnections[deviceId] ?: return
|
||||
if (!connection.connected) return
|
||||
try {
|
||||
writeMutex.withLock {
|
||||
connection.outputStream?.write(buffer.toByteArray())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "$tag: Write failed ${e.message}")
|
||||
connection.connected = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: WithoutExtParams) {
|
||||
reporterScope.launch {
|
||||
if (!isConnected(BtService.ROTATOR)) return@launch
|
||||
val newElevation = if (elevation > 0.0) elevation else 0.0
|
||||
val azimuthString = intToStringWithLeadingZeroes(azimuth.toInt())
|
||||
val elevationString = intToStringWithLeadingZeroes(newElevation.toInt())
|
||||
val buffer = format
|
||||
.replace($$"$AZ", azimuthString)
|
||||
.replace($$"$EL", elevationString)
|
||||
.replace("\\r", "\r")
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\t", "\t")
|
||||
write(BtService.ROTATOR, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportFrequency(format: String, frequency: Long, params: WithoutExtParams) {
|
||||
reporterScope.launch {
|
||||
if (!isConnected(BtService.FREQUENCY)) return@launch
|
||||
val buffer = format
|
||||
.replace($$"$FREQ", frequency.toString())
|
||||
.replace("\\r", "\r")
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\t", "\t")
|
||||
write(BtService.FREQUENCY, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun intToStringWithLeadingZeroes(value: Int): String {
|
||||
return if (value > 0) {
|
||||
if (value < 10) "00$value" else if (value < 100) "0$value" else "$value"
|
||||
} else {
|
||||
val absValue = abs(value)
|
||||
if (value > -10) "-00$absValue" else if (value > -100) "-0$absValue" else "-$absValue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.framework
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.repository.BtService
|
||||
import com.rtbishop.look4sat.core.domain.repository.ExtendedParams
|
||||
import com.rtbishop.look4sat.core.domain.repository.IReporterRepo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.SocketChannel
|
||||
|
||||
enum class NetworkService { ROTATOR, FREQUENCY }
|
||||
|
||||
data class SocketConnection(
|
||||
var socket: SocketChannel? = null,
|
||||
var connected: Boolean = false,
|
||||
var connecting: Boolean = false,
|
||||
var connectionJob: Job? = null
|
||||
)
|
||||
|
||||
class NetworkReporter(private val reporterScope: CoroutineScope) : IReporterRepo<ExtendedParams> {
|
||||
private val serviceToAddress = mutableMapOf<NetworkService, String>()
|
||||
private val connections = mutableMapOf<String, SocketConnection>()
|
||||
private val writeMutex = Mutex()
|
||||
|
||||
fun isConnected(service: NetworkService): Boolean {
|
||||
val addr = serviceToAddress[service] ?: return false
|
||||
return connections[addr]?.connected == true
|
||||
}
|
||||
|
||||
private fun getOrCreateConnection(addr: String): SocketConnection {
|
||||
return connections.getOrPut(addr) { SocketConnection() }
|
||||
}
|
||||
|
||||
fun connect(service: NetworkService, server: String, port: Int) {
|
||||
val addr = "$server:$port"
|
||||
serviceToAddress[service] = addr
|
||||
val connection = getOrCreateConnection(addr)
|
||||
if (connection.connected || connection.connecting) return
|
||||
connection.connectionJob = reporterScope.launch {
|
||||
try {
|
||||
connection.connecting = true
|
||||
val socket = SocketChannel.open(InetSocketAddress(server, port))
|
||||
connection.socket = socket
|
||||
connection.connected = true
|
||||
println("NetworkReporter: $service connected to $addr")
|
||||
} catch (e: Exception) {
|
||||
println("NetworkReporter connect error: ${e.message}")
|
||||
connection.connected = false
|
||||
} finally {
|
||||
connection.connecting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun write(service: NetworkService, command: String) {
|
||||
val addr = serviceToAddress[service] ?: return
|
||||
val connection = connections[addr] ?: return
|
||||
if (!connection.connected) return
|
||||
try {
|
||||
writeMutex.withLock {
|
||||
val buffer = ByteBuffer.wrap("\\$command\n".toByteArray())
|
||||
connection.socket?.write(buffer)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("NetworkReporter write error: ${e.message}")
|
||||
connection.connected = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun isConnected(service: BtService): Boolean = false
|
||||
|
||||
override fun isConnecting(service: BtService): Boolean = false
|
||||
|
||||
override fun connect(service: BtService, deviceId: String) {}
|
||||
|
||||
override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: ExtendedParams) {
|
||||
reporterScope.launch {
|
||||
connect(NetworkService.ROTATOR, params.server, params.port)
|
||||
if (!isConnected(NetworkService.ROTATOR)) return@launch
|
||||
val el = if (elevation > 0.0) elevation else 0.0
|
||||
val command = format
|
||||
.replace("\$AZ", azimuth.toString())
|
||||
.replace("\$EL", el.toString())
|
||||
write(NetworkService.ROTATOR, command)
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportFrequency(format: String, frequency: Long, params: ExtendedParams) {
|
||||
reporterScope.launch {
|
||||
connect(NetworkService.FREQUENCY, params.server, params.port)
|
||||
if (!isConnected(NetworkService.FREQUENCY)) return@launch
|
||||
val command = format.replace("\$FREQ", frequency.toString())
|
||||
write(NetworkService.FREQUENCY, command)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.rtbishop.look4sat.core.data.injection
|
||||
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
import android.location.LocationManager
|
||||
import androidx.room.Room
|
||||
import com.rtbishop.look4sat.core.data.database.Look4SatDb
|
||||
import com.rtbishop.look4sat.core.data.framework.BluetoothReporter
|
||||
import com.rtbishop.look4sat.core.data.framework.NetworkReporter
|
||||
import com.rtbishop.look4sat.core.data.repository.DatabaseRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SatelliteRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SelectionRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SensorsRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SettingsRepo
|
||||
import com.rtbishop.look4sat.core.data.source.LocalSource
|
||||
import com.rtbishop.look4sat.core.data.source.RemoteSource
|
||||
import com.rtbishop.look4sat.core.data.usecase.AddToCalendar
|
||||
import com.rtbishop.look4sat.core.data.usecase.ShowToast
|
||||
import com.rtbishop.look4sat.core.domain.repository.ExtendedParams
|
||||
import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||
import com.rtbishop.look4sat.core.domain.repository.IReporterRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.WithoutExtParams
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
import com.rtbishop.look4sat.core.domain.utility.DataParser
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class MainContainer(private val context: Context) : IMainContainer {
|
||||
|
||||
private val localSource = provideLocalSource()
|
||||
private val mainHandler = CoroutineExceptionHandler { _, error -> println("MainHandler: $error") }
|
||||
override val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
|
||||
override val settingsRepo = provideSettingsRepo()
|
||||
override val selectionRepo = provideSelectionRepo()
|
||||
override val satelliteRepo = provideSatelliteRepo()
|
||||
override val databaseRepo = provideDatabaseRepo()
|
||||
|
||||
override fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context)
|
||||
|
||||
override fun provideShowToast(): IShowToast = ShowToast(context)
|
||||
|
||||
override fun provideBluetoothReporter(): IReporterRepo<WithoutExtParams> {
|
||||
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO))
|
||||
}
|
||||
|
||||
override fun provideNetworkReporter(): IReporterRepo<ExtendedParams> {
|
||||
return NetworkReporter(CoroutineScope(Dispatchers.IO))
|
||||
}
|
||||
|
||||
override fun provideSensorsRepo(): ISensorsRepo {
|
||||
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
|
||||
return SensorsRepo(manager, sensor)
|
||||
}
|
||||
|
||||
private fun provideDatabaseRepo(): IDatabaseRepo {
|
||||
val dbDispatcher = Dispatchers.Default
|
||||
val dataParser = DataParser(dbDispatcher)
|
||||
val remoteSource = provideRemoteSource()
|
||||
return DatabaseRepo(dbDispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
}
|
||||
|
||||
private fun provideLocalSource(): ILocalSource {
|
||||
val builder = Room.databaseBuilder(context, Look4SatDb::class.java, "Look4SatDBv400")
|
||||
val database = builder.fallbackToDestructiveMigration(false).build()
|
||||
return LocalSource(database.look4SatDao())
|
||||
}
|
||||
|
||||
private fun provideRemoteSource(): IRemoteSource {
|
||||
return RemoteSource(Dispatchers.IO, context.contentResolver, OkHttpClient.Builder().build())
|
||||
}
|
||||
|
||||
private fun provideSatelliteRepo(): ISatelliteRepo {
|
||||
return SatelliteRepo(Dispatchers.Default, localSource, settingsRepo)
|
||||
}
|
||||
|
||||
private fun provideSelectionRepo(): ISelectionRepo {
|
||||
return SelectionRepo(Dispatchers.Default, localSource, settingsRepo)
|
||||
}
|
||||
|
||||
private fun provideSettingsRepo(): ISettingsRepo {
|
||||
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
val appPrefsFileName = "${context.packageName}_preferences"
|
||||
val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE)
|
||||
val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.4"
|
||||
return SettingsRepo(manager, appPreferences, appVersionName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.DatabaseState
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
|
||||
import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import com.rtbishop.look4sat.core.domain.source.Sources
|
||||
import com.rtbishop.look4sat.core.domain.utility.DataParser
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
class DatabaseRepo(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val dataParser: DataParser,
|
||||
private val localSource: ILocalSource,
|
||||
private val remoteSource: IRemoteSource,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
) : IDatabaseRepo {
|
||||
|
||||
override suspend fun updateTLEFromFile(uri: String) = withContext(dispatcher) {
|
||||
val importedSatellites = remoteSource.getFileStream(uri)?.let { dataParser.parseTLEStream(it) }
|
||||
importedSatellites?.let { localSource.insertEntries(it) }
|
||||
setUpdateSuccessful(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
override suspend fun updateTransceiversFromFile(uri: String) = withContext(dispatcher) {
|
||||
val radios = remoteSource.getFileStream(uri)?.let { dataParser.parseJSONStream(it) }
|
||||
radios?.let {
|
||||
if(it.isNotEmpty()) {
|
||||
localSource.deleteRadios()
|
||||
localSource.insertRadios(it)
|
||||
}
|
||||
}
|
||||
setUpdateSuccessful(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
override suspend fun updateFromRemote() = withContext(dispatcher) {
|
||||
val importedEntries = mutableListOf<OrbitalData>()
|
||||
val importedRadios = mutableListOf<SatRadio>()
|
||||
val tleUrls = Sources.satelliteDataUrls.toMutableMap().apply {
|
||||
if (settingsRepo.dataSourcesSettings.value.useCustomTLE) {
|
||||
this["Other"] = settingsRepo.dataSourcesSettings.value.tleUrl
|
||||
}
|
||||
}
|
||||
val jobsMap = tleUrls
|
||||
.filter { (_, url) -> url.isNotEmpty() }
|
||||
.mapValues { (_, url) -> async { remoteSource.getNetworkStream(url) } }
|
||||
val radioUrls = buildList {
|
||||
add(Sources.RADIO_DATA_URL)
|
||||
if (settingsRepo.dataSourcesSettings.value.useCustomTransceivers) {
|
||||
add(settingsRepo.dataSourcesSettings.value.transceiversUrl)
|
||||
}
|
||||
}
|
||||
val jobRadios = radioUrls.associateWith { url -> async { remoteSource.getNetworkStream(url) } }
|
||||
// parse
|
||||
jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
|
||||
entry.value?.let { stream ->
|
||||
when (val type = entry.key) {
|
||||
"Amsat", "R4UAB", "Other" -> {
|
||||
// parse tle stream
|
||||
val satellites = dataParser.parseTLEStream(stream)
|
||||
val catnums = satellites.map { it.catnum }
|
||||
settingsRepo.setSatelliteTypeIds(type, catnums)
|
||||
importedEntries.addAll(satellites)
|
||||
}
|
||||
|
||||
"McCants", "Classified" -> {
|
||||
// unzip and parse tle stream
|
||||
val unzipped = ZipInputStream(stream).apply { nextEntry }
|
||||
val satellites = dataParser.parseTLEStream(unzipped)
|
||||
val catnums = satellites.map { it.catnum }
|
||||
settingsRepo.setSatelliteTypeIds(type, catnums)
|
||||
importedEntries.addAll(satellites)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// parse csv stream
|
||||
val satellites = dataParser.parseCSVStream(stream)
|
||||
val catnums = satellites.map { it.catnum }
|
||||
settingsRepo.setSatelliteTypeIds(type, catnums)
|
||||
importedEntries.addAll(satellites)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jobRadios.values.forEach { job ->
|
||||
job.await()?.let {
|
||||
importedRadios.addAll(dataParser.parseJSONStream(it))
|
||||
}
|
||||
}
|
||||
// insert
|
||||
localSource.insertEntries(importedEntries)
|
||||
localSource.insertRadios(importedRadios)
|
||||
setUpdateSuccessful(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
override suspend fun clearAllData() = withContext(dispatcher) {
|
||||
localSource.deleteEntries()
|
||||
localSource.deleteRadios()
|
||||
setUpdateSuccessful(0L)
|
||||
}
|
||||
|
||||
private suspend fun setUpdateSuccessful(timestamp: Long) = withContext(dispatcher) {
|
||||
val numberOfRadios = localSource.getRadiosTotal()
|
||||
val numberOfSatellites = localSource.getEntriesTotal()
|
||||
settingsRepo.updateDatabaseState(DatabaseState(numberOfRadios, numberOfSatellites, timestamp))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.domain.utility.round
|
||||
import com.rtbishop.look4sat.core.domain.utility.toDegrees
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SatelliteRepo(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val localStorage: ILocalSource,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
) : ISatelliteRepo {
|
||||
|
||||
private val _passes = MutableStateFlow<List<OrbitalPass>>(emptyList())
|
||||
override val passes: StateFlow<List<OrbitalPass>> = _passes
|
||||
|
||||
private val _satellites = MutableStateFlow<List<OrbitalObject>>(emptyList())
|
||||
override val satellites: StateFlow<List<OrbitalObject>> = _satellites
|
||||
|
||||
override suspend fun getRadiosWithId(id: Int) = localStorage.getRadiosWithId(id)
|
||||
|
||||
override suspend fun initRepository() = withContext(dispatcher) {
|
||||
settingsRepo.selectedIds.collect { selectedIds ->
|
||||
_satellites.update { localStorage.getEntriesWithIds(selectedIds) }
|
||||
val (hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
|
||||
val timeNow = 1000 * ((System.currentTimeMillis() + 500) / 1000)
|
||||
calculatePasses(timeNow, hoursAhead, minElevation, modes)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos {
|
||||
return withContext(dispatcher) { sat.getPosition(pos, time) }
|
||||
}
|
||||
|
||||
override suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos> {
|
||||
return withContext(dispatcher) {
|
||||
val positions = mutableListOf<OrbitalPos>()
|
||||
var currentTime = start
|
||||
while (currentTime < end) {
|
||||
positions.add(sat.getPosition(pos, currentTime))
|
||||
currentTime += 15000
|
||||
}
|
||||
positions
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRadios(
|
||||
sat: OrbitalObject,
|
||||
pos: GeoPos,
|
||||
radios: List<SatRadio>,
|
||||
time: Long
|
||||
): List<SatRadio> {
|
||||
return withContext(dispatcher) {
|
||||
val satPos = sat.getPosition(pos, time)
|
||||
val copiedList = radios.map { it.copy() }
|
||||
copiedList.forEach { transmitter ->
|
||||
transmitter.downlinkLow?.let { transmitter.downlinkLow = satPos.getDownlinkFreq(it) }
|
||||
transmitter.downlinkHigh?.let { transmitter.downlinkHigh = satPos.getDownlinkFreq(it) }
|
||||
transmitter.uplinkLow?.let { transmitter.uplinkLow = satPos.getUplinkFreq(it) }
|
||||
transmitter.uplinkHigh?.let { transmitter.uplinkHigh = satPos.getUplinkFreq(it) }
|
||||
}
|
||||
copiedList.map { it.copy() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun processPasses(passList: List<OrbitalPass>, time: Long): List<OrbitalPass> {
|
||||
return withContext(dispatcher) {
|
||||
passList.forEach { pass ->
|
||||
if (!pass.isDeepSpace) {
|
||||
val timeStart = pass.aosTime
|
||||
if (time > timeStart) {
|
||||
val deltaNow = time.minus(timeStart).toFloat()
|
||||
val deltaTotal = pass.losTime.minus(timeStart).toFloat()
|
||||
pass.progress = (deltaNow / deltaTotal).round(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
passList.filter { pass -> pass.progress < 1.0 }.map { it.copy() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double, modes: List<String>) {
|
||||
if (_satellites.value.isNotEmpty()) {
|
||||
withContext(dispatcher) {
|
||||
val newPasses = mutableListOf<OrbitalPass>()
|
||||
val idsWithModes = localStorage.getIdsWithModes(modes)
|
||||
_satellites.value.forEach { satellite ->
|
||||
if (idsWithModes.isEmpty() || satellite.data.catnum in idsWithModes) {
|
||||
newPasses.addAll(satellite.getPasses(settingsRepo.stationPosition.value, time, hoursAhead))
|
||||
}
|
||||
}
|
||||
_passes.update { newPasses.filter(time, hoursAhead, minElevation) }
|
||||
}
|
||||
} else {
|
||||
_passes.update { emptyList() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun OrbitalObject.getPasses(pos: GeoPos, time: Long, hours: Int): List<OrbitalPass> {
|
||||
val passes = mutableListOf<OrbitalPass>()
|
||||
val endDate = time + hours * 60L * 60L * 1000L
|
||||
val quarterOrbitMin = (this.data.orbitalPeriod / 4.0).toInt()
|
||||
var startDate = time
|
||||
var shouldRewind = true
|
||||
var lastAosDate: Long
|
||||
var count = 0
|
||||
if (this.willBeSeen(pos)) {
|
||||
if (this.data.isDeepSpace) {
|
||||
passes.add(getGeoPass(this, pos, time))
|
||||
} else {
|
||||
do {
|
||||
if (count > 0) shouldRewind = false
|
||||
val pass = getLeoPass(this, pos, startDate, shouldRewind)
|
||||
lastAosDate = pass.aosTime
|
||||
passes.add(pass)
|
||||
startDate = pass.losTime + (quarterOrbitMin * 3) * 60L * 1000L
|
||||
count++
|
||||
} while (lastAosDate < endDate)
|
||||
}
|
||||
}
|
||||
return passes
|
||||
}
|
||||
|
||||
private fun List<OrbitalPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<OrbitalPass> {
|
||||
val timeFuture = time + (hoursAhead * 60L * 60L * 1000L)
|
||||
return this.filter { it.losTime > time }.filter { it.aosTime < timeFuture }
|
||||
.filter { it.maxElevation > minElev }.sortedBy { it.aosTime }
|
||||
}
|
||||
|
||||
private fun getGeoPass(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPass {
|
||||
val satPos = sat.getPosition(pos, time)
|
||||
val aos = time - 24 * 60L * 60L * 1000L
|
||||
val los = time + 24 * 60L * 60L * 1000L // val tca = (aos + los) / 2
|
||||
val az = satPos.azimuth.toDegrees().round(1)
|
||||
val elev = satPos.elevation.toDegrees().round(1)
|
||||
val alt = satPos.altitude
|
||||
return OrbitalPass(aos, az, los, az, alt.toInt(), elev, sat)
|
||||
}
|
||||
|
||||
private fun getLeoPass(sat: OrbitalObject, pos: GeoPos, time: Long, rewind: Boolean): OrbitalPass {
|
||||
val quarterOrbitMin = (sat.data.orbitalPeriod / 4.0).toInt()
|
||||
var calendarTimeMillis = time
|
||||
var elevation: Double
|
||||
var maxElevation = 0.0
|
||||
var alt = 0.0 // var tcaAz = 0.0
|
||||
// rewind 1/4 of an orbit
|
||||
if (rewind) calendarTimeMillis += -quarterOrbitMin * 60L * 1000L
|
||||
|
||||
var satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
if (satPos.elevation > 0.0) {
|
||||
// move forward in 30 second intervals until the sat goes below the horizon
|
||||
do {
|
||||
calendarTimeMillis += 30 * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
} while (satPos.elevation > 0.0)
|
||||
// move forward 3/4 of an orbit
|
||||
calendarTimeMillis += quarterOrbitMin * 3 * 60L * 1000L
|
||||
}
|
||||
|
||||
// find the next time sat comes above the horizon
|
||||
do {
|
||||
calendarTimeMillis += 60L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
elevation = satPos.elevation
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation
|
||||
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
|
||||
}
|
||||
} while (satPos.elevation < 0.0)
|
||||
|
||||
// refine to 1 second
|
||||
calendarTimeMillis += -60L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 1L * 500L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
elevation = satPos.elevation
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation
|
||||
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
|
||||
}
|
||||
} while (satPos.elevation < 0.0)
|
||||
|
||||
val aos = 1000 * ((satPos.time + 500) / 1000)
|
||||
val aosAz = satPos.azimuth.toDegrees().round(1)
|
||||
|
||||
// find when sat goes below
|
||||
do {
|
||||
calendarTimeMillis += 30L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
elevation = satPos.elevation
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation
|
||||
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
|
||||
}
|
||||
} while (satPos.elevation > 0.0)
|
||||
|
||||
// refine to 1 second
|
||||
calendarTimeMillis += -30L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 1L * 500L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
elevation = satPos.elevation
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation
|
||||
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
|
||||
}
|
||||
} while (satPos.elevation > 0.0)
|
||||
|
||||
val los = 1000 * ((satPos.time + 500) / 1000) // val tca = (aos + los) / 2
|
||||
val losAz = satPos.azimuth.toDegrees().round(1)
|
||||
val elev = maxElevation.toDegrees().round(1)
|
||||
return OrbitalPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatItem
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.domain.source.Sources
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SelectionRepo(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val localSource: ILocalSource,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
) : ISelectionRepo {
|
||||
|
||||
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
|
||||
private val currentTypes = MutableStateFlow(settingsRepo.selectedTypes.value)
|
||||
private val currentQuery = MutableStateFlow("")
|
||||
private val itemsWithTypes = currentTypes.flatMapLatest { types: List<String> ->
|
||||
currentItems.map { items -> items.filterByTypes(types) }
|
||||
}
|
||||
private val itemsWithQuery = currentQuery.flatMapLatest { query ->
|
||||
itemsWithTypes.map { items -> items.filterByQuery(query) }
|
||||
}
|
||||
|
||||
override fun getCurrentTypes() = currentTypes.value
|
||||
|
||||
override fun getTypesList() = Sources.satelliteDataUrls.keys.sorted().toMutableList().apply {
|
||||
removeAt(0)
|
||||
}
|
||||
|
||||
override suspend fun getEntriesFlow() = withContext(dispatcher) {
|
||||
val selectedIds = settingsRepo.selectedIds.value
|
||||
currentItems.value = localSource.getEntriesList().map { item ->
|
||||
item.copy(isSelected = item.catnum in selectedIds)
|
||||
}
|
||||
return@withContext itemsWithQuery
|
||||
}
|
||||
|
||||
override suspend fun setTypes(types: List<String>) {
|
||||
currentTypes.value = types
|
||||
settingsRepo.setSelectedTypes(types)
|
||||
}
|
||||
|
||||
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 ->
|
||||
if (item.catnum in ids) item.copy(isSelected = isTicked) else item
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveSelection() = withContext(dispatcher) {
|
||||
val currentSelection = currentItems.value.filter { it.isSelected }.map { it.catnum }
|
||||
settingsRepo.setSelectedIds(currentSelection)
|
||||
}
|
||||
|
||||
private suspend fun List<SatItem>.filterByTypes(types: List<String>) = withContext(dispatcher) {
|
||||
if (types.isEmpty()) return@withContext this@filterByTypes
|
||||
val catnums = settingsRepo.getSatelliteTypesIds(types)
|
||||
if (catnums.isEmpty()) return@withContext this@filterByTypes
|
||||
return@withContext this@filterByTypes.filter { item -> item.catnum in catnums }
|
||||
}
|
||||
|
||||
private suspend fun List<SatItem>.filterByQuery(query: String) = withContext(dispatcher) {
|
||||
if (query.isBlank()) return@withContext this@filterByQuery
|
||||
return@withContext try {
|
||||
this@filterByQuery.filter { it.catnum == query.toInt() }
|
||||
} catch (_: Exception) {
|
||||
this@filterByQuery.filter { item -> item.name.lowercase().contains(query.lowercase()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.repository
|
||||
|
||||
import android.hardware.GeomagneticField
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.predict.RAD2DEG
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlin.math.round
|
||||
|
||||
class SensorsRepo(private val sensorManager: SensorManager, private val sensor: Sensor?) :
|
||||
SensorEventListener, ISensorsRepo {
|
||||
|
||||
private val _orientation = MutableStateFlow(Pair(0f, 0f))
|
||||
private val rotationMatrix = FloatArray(9)
|
||||
private val orientationValues = FloatArray(3)
|
||||
private var sensorAccuracy = SensorManager.SENSOR_STATUS_UNRELIABLE
|
||||
|
||||
override val orientation: StateFlow<Pair<Float, Float>> = _orientation
|
||||
|
||||
override fun getMagDeclination(geoPos: GeoPos, time: Long): Float {
|
||||
val latitude = geoPos.latitude.toFloat()
|
||||
val longitude = geoPos.longitude.toFloat()
|
||||
return GeomagneticField(latitude, longitude, geoPos.altitude.toFloat(), time).declination
|
||||
}
|
||||
|
||||
override fun enableSensor() {
|
||||
sensor?.let { sensorManager.registerListener(this, it, 8000) }
|
||||
}
|
||||
|
||||
override fun disableSensor() = sensorManager.unregisterListener(this)
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
||||
sensorAccuracy = accuracy
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) = when {
|
||||
sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> {}
|
||||
event.sensor == sensor -> updateOrientation(event.values)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
private fun updateOrientation(rotationVector: FloatArray) {
|
||||
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
|
||||
SensorManager.getOrientation(rotationMatrix, orientationValues)
|
||||
val azimuth = (orientationValues[0] * RAD2DEG).toFloat()
|
||||
val pitch = (orientationValues[1] * RAD2DEG).toFloat() // roll [2]
|
||||
val magneticAzimuth = (azimuth + 360f) % 360f
|
||||
val roundedAzimuth = round(magneticAzimuth * 10) / 10
|
||||
val roundedPitch = round(pitch * 10) / 10
|
||||
_orientation.value = Pair(roundedAzimuth, roundedPitch)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.repository
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.DatabaseState
|
||||
import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.PassesSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
||||
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
|
||||
import com.rtbishop.look4sat.core.domain.utility.round
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class SettingsRepo(
|
||||
private val locationManager: LocationManager,
|
||||
private val preferences: SharedPreferences,
|
||||
override val appVersionName: String
|
||||
) : ISettingsRepo, LocationListenerCompat {
|
||||
|
||||
private val keyBluetoothRotatorAddress = "bluetoothAddress"
|
||||
private val keyBluetoothRotatorName = "bluetoothName"
|
||||
private val keyBluetoothRotatorFormat = "bluetoothFormat"
|
||||
private val keyBluetoothRotatorState = "bluetoothState"
|
||||
private val keyBluetoothFrequencyState = "bluetoothFrequencyState"
|
||||
private val keyBluetoothFrequencyAddress = "bluetoothFrequencyAddress"
|
||||
private val keyBluetoothFrequencyFormat = "bluetoothFrequencyFormat"
|
||||
private val keyFilterHoursAhead = "filterHoursAhead"
|
||||
private val keyFilterMinElevation = "filterMinElevation"
|
||||
private val keyNumberOfRadios = "numberOfRadios"
|
||||
private val keyNumberOfSatellites = "numberOfSatellites"
|
||||
private val keyRotatorAddress = "rotatorAddress"
|
||||
private val keyRotatorPort = "rotatorPort"
|
||||
private val keyRotatorState = "rotatorState"
|
||||
private val keyRotatorFormat = "rotatorFormat"
|
||||
private val keyFrequencyState = "frequencyState"
|
||||
private val keyFrequencyAddress = "frequencyAddress"
|
||||
private val keyFrequencyPort = "frequencyPort"
|
||||
private val keyFrequencyFormat = "frequencyFormat"
|
||||
private val keySelectedIds = "selectedIds"
|
||||
private val keySelectedTypes = "selectedTypes"
|
||||
private val keySelectedModes = "selectedModes"
|
||||
private val keyStateOfAutoUpdate = "stateOfAutoUpdate"
|
||||
private val keyStateOfSensors = "stateOfSensors"
|
||||
private val keyStateOfSweep = "stateOfSweep"
|
||||
private val keyStateOfUtc = "stateOfUtc"
|
||||
private val keyStateOfLightTheme = "stateOfLightTheme"
|
||||
private val keyStationAltitude = "stationAltitude"
|
||||
private val keyStationLatitude = "stationLatitude"
|
||||
private val keyStationLongitude = "stationLongitude"
|
||||
private val keyStationQth = "stationQth"
|
||||
private val keyStationTimestamp = "stationTimestamp"
|
||||
private val keyUpdateTimestamp = "updateTimestamp"
|
||||
private val keyShouldSeeWarning = "shouldSeeWarning"
|
||||
private val keyShouldSeeWhatsNew = "shouldSeeWhatsNew_v$appVersionName"
|
||||
private val keyUseCustomTle = "useCustomTle"
|
||||
private val keyUseCustomTransceivers = "useCustomTransceivers"
|
||||
private val keyTleUrl = "tleUrl"
|
||||
private val keyTransceiversUrl = "transceiversUrl"
|
||||
private val separatorComma = ","
|
||||
|
||||
//region # Satellites selection settings
|
||||
private val _satelliteSelection = MutableStateFlow(getSelectedIds())
|
||||
private val _typesSelection = MutableStateFlow(getSelectedTypes())
|
||||
override val selectedIds: StateFlow<List<Int>> = _satelliteSelection
|
||||
override val selectedTypes: StateFlow<List<String>> = _typesSelection
|
||||
|
||||
override fun setSelectedIds(ids: List<Int>) {
|
||||
val selectionString = ids.joinToString(separatorComma)
|
||||
preferences.edit { putString(keySelectedIds, selectionString) }
|
||||
_satelliteSelection.value = ids
|
||||
}
|
||||
|
||||
override fun setSelectedTypes(types: List<String>) {
|
||||
val typesString = types.joinToString(separatorComma)
|
||||
preferences.edit { putString(keySelectedTypes, typesString) }
|
||||
_typesSelection.value = types
|
||||
}
|
||||
|
||||
private fun getSelectedIds(): List<Int> {
|
||||
val selectionString = preferences.getString(keySelectedIds, null)
|
||||
if (selectionString.isNullOrEmpty()) return emptyList()
|
||||
return selectionString.split(separatorComma).map { it.toInt() }
|
||||
}
|
||||
|
||||
private fun getSelectedTypes(): List<String> {
|
||||
val typesString = preferences.getString(keySelectedTypes, null)
|
||||
if (typesString.isNullOrEmpty()) return listOf("Amateur")
|
||||
return typesString.split(separatorComma)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region # Passes filter settings
|
||||
private val _passesSettings = MutableStateFlow(getPassesSettings())
|
||||
override val passesSettings: StateFlow<PassesSettings> = _passesSettings
|
||||
|
||||
override fun setPassesSettings(settings: PassesSettings) = preferences.edit {
|
||||
putInt(keyFilterHoursAhead, settings.hoursAhead)
|
||||
putLong(keyFilterMinElevation, settings.minElevation.toRawBits())
|
||||
putString(keySelectedModes, settings.selectedModes.joinToString(separatorComma))
|
||||
_passesSettings.value = settings
|
||||
}
|
||||
|
||||
private fun getPassesSettings(): PassesSettings {
|
||||
val hoursAhead = preferences.getInt(keyFilterHoursAhead, 24)
|
||||
val minElevation = Double.fromBits(preferences.getLong(keyFilterMinElevation, 16.0.toRawBits()))
|
||||
val selectedModesString = preferences.getString(keySelectedModes, null)
|
||||
val selectedModes = selectedModesString?.split(separatorComma)?.sorted() ?: emptyList()
|
||||
return PassesSettings(hoursAhead, minElevation, selectedModes)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region # Station position settings
|
||||
private val _stationPosition = MutableStateFlow(getStationPosition())
|
||||
private val providerDef = LocationManager.PASSIVE_PROVIDER
|
||||
private val providerGps = LocationManager.GPS_PROVIDER
|
||||
private val providerNet = LocationManager.NETWORK_PROVIDER
|
||||
override val stationPosition: StateFlow<GeoPos> = _stationPosition
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
setStationPosition(location.latitude, location.longitude, location.altitude)
|
||||
}
|
||||
|
||||
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean {
|
||||
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
|
||||
val locator = positionToQth(latitude, newLongitude) ?: return false
|
||||
setStationPosition(latitude, newLongitude, altitude, locator)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setStationPosition(): Boolean {
|
||||
if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
|
||||
try {
|
||||
val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps)
|
||||
val hasNet = LocationManagerCompat.hasProvider(locationManager, providerNet)
|
||||
val provider = if (hasGps) providerGps else if (hasNet) providerNet else providerDef
|
||||
val location = locationManager.getLastKnownLocation(providerDef)
|
||||
if (location == null || System.currentTimeMillis() - location.time > 600_000L) {
|
||||
println("Requesting location for $provider provider")
|
||||
locationManager.requestLocationUpdates(provider, 0L, 0f, this)
|
||||
} else {
|
||||
setStationPosition(location.latitude, location.longitude, location.altitude)
|
||||
}
|
||||
} catch (exception: SecurityException) {
|
||||
println("No permissions were given - $exception")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setStationPosition(locator: String): Boolean {
|
||||
val position = qthToPosition(locator) ?: return false
|
||||
setStationPosition(position.latitude, position.longitude, 0.0, locator)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getStationPosition(): GeoPos {
|
||||
val latitude = (preferences.getString(keyStationLatitude, null) ?: "0.0").toDouble()
|
||||
val longitude = (preferences.getString(keyStationLongitude, null) ?: "0.0").toDouble()
|
||||
val altitude = (preferences.getString(keyStationAltitude, null) ?: "0.0").toDouble()
|
||||
val qthLocator = preferences.getString(keyStationQth, null) ?: "JJ00aa"
|
||||
val timestamp = preferences.getLong(keyStationTimestamp, 0L)
|
||||
return GeoPos(latitude, longitude, altitude, qthLocator, timestamp)
|
||||
}
|
||||
|
||||
private fun setStationPosition(latitude: Double, longitude: Double, altitude: Double, locator: String) {
|
||||
val newLat = latitude.round(4)
|
||||
val newLon = longitude.round(4)
|
||||
val newAlt = altitude.round(1)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
println("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
|
||||
setStationPosition(GeoPos(newLat, newLon, newAlt, locator, timestamp))
|
||||
}
|
||||
|
||||
private fun setStationPosition(stationPos: GeoPos) = preferences.edit {
|
||||
putString(keyStationLatitude, stationPos.latitude.toString())
|
||||
putString(keyStationLongitude, stationPos.longitude.toString())
|
||||
putString(keyStationAltitude, stationPos.altitude.toString())
|
||||
putString(keyStationQth, stationPos.qthLocator)
|
||||
putLong(keyStationTimestamp, stationPos.timestamp)
|
||||
_stationPosition.value = stationPos
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region # Database update settings
|
||||
private val _databaseState = MutableStateFlow(getDatabaseState())
|
||||
override val databaseState: StateFlow<DatabaseState> = _databaseState
|
||||
|
||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> {
|
||||
val idsSet = mutableSetOf<Int>()
|
||||
types.forEach { type ->
|
||||
val typeString = preferences.getString("type$type", null)
|
||||
val typeIds = if (typeString.isNullOrBlank()) {
|
||||
emptyList()
|
||||
} else {
|
||||
typeString.split(separatorComma).map { it.toInt() }
|
||||
}
|
||||
idsSet.addAll(typeIds)
|
||||
}
|
||||
return idsSet.toList()
|
||||
}
|
||||
|
||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
|
||||
if (type == "All") return
|
||||
val typesString = ids.joinToString(separatorComma)
|
||||
preferences.edit { putString("type$type", typesString) }
|
||||
}
|
||||
|
||||
override fun updateDatabaseState(state: DatabaseState) = preferences.edit {
|
||||
putInt(keyNumberOfSatellites, state.numberOfSatellites)
|
||||
putInt(keyNumberOfRadios, state.numberOfRadios)
|
||||
putLong(keyUpdateTimestamp, state.updateTimestamp)
|
||||
_databaseState.value = state
|
||||
}
|
||||
|
||||
private fun getDatabaseState(): DatabaseState {
|
||||
val numberOfRadios = preferences.getInt(keyNumberOfRadios, 0)
|
||||
val numberOfSatellites = preferences.getInt(keyNumberOfSatellites, 0)
|
||||
val updateTimestamp = preferences.getLong(keyUpdateTimestamp, 0L)
|
||||
return DatabaseState(numberOfRadios, numberOfSatellites, updateTimestamp)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region # RC settings
|
||||
private val _rcSettings = MutableStateFlow(getRCSettings())
|
||||
override val rcSettings: StateFlow<RCSettings> = _rcSettings
|
||||
|
||||
override fun setBluetoothRotatorAddress(value: String) {
|
||||
preferences.edit { putString(keyBluetoothRotatorAddress, value) }
|
||||
_rcSettings.update { it.copy(bluetoothRotatorAddress = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothRotatorFormat(value: String) {
|
||||
preferences.edit { putString(keyBluetoothRotatorFormat, value) }
|
||||
_rcSettings.update { it.copy(bluetoothRotatorFormat = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothRotatorName(value: String) {
|
||||
preferences.edit { putString(keyBluetoothRotatorName, value) }
|
||||
_rcSettings.update { it.copy(bluetoothRotatorName = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothRotatorState(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyBluetoothRotatorState, value) }
|
||||
_rcSettings.update { it.copy(bluetoothRotatorState = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothFrequencyState(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyBluetoothFrequencyState, value) }
|
||||
_rcSettings.update { it.copy(bluetoothFrequencyState = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothFrequencyAddress(value: String) {
|
||||
preferences.edit { putString(keyBluetoothFrequencyAddress, value) }
|
||||
_rcSettings.update { it.copy(bluetoothFrequencyAddress = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothFrequencyFormat(value: String) {
|
||||
preferences.edit { putString(keyBluetoothFrequencyFormat, value) }
|
||||
_rcSettings.update { it.copy(bluetoothFrequencyFormat = value) }
|
||||
}
|
||||
|
||||
override fun setRotatorAddress(value: String) {
|
||||
preferences.edit { putString(keyRotatorAddress, value) }
|
||||
_rcSettings.update { it.copy(rotatorAddress = value) }
|
||||
}
|
||||
|
||||
override fun setRotatorPort(value: String) {
|
||||
preferences.edit { putString(keyRotatorPort, value) }
|
||||
_rcSettings.update { it.copy(rotatorPort = value) }
|
||||
}
|
||||
|
||||
override fun setRotatorState(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyRotatorState, value) }
|
||||
_rcSettings.update { it.copy(rotatorState = value) }
|
||||
}
|
||||
|
||||
override fun setRotatorFormat(value: String) {
|
||||
preferences.edit { putString(keyRotatorFormat, value) }
|
||||
_rcSettings.update { it.copy(rotatorFormat = value) }
|
||||
}
|
||||
|
||||
override fun setFrequencyState(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyFrequencyState, value) }
|
||||
_rcSettings.update { it.copy(frequencyState = value) }
|
||||
}
|
||||
|
||||
override fun setFrequencyAddress(value: String) {
|
||||
preferences.edit { putString(keyFrequencyAddress, value) }
|
||||
_rcSettings.update { it.copy(frequencyAddress = value) }
|
||||
}
|
||||
|
||||
override fun setFrequencyPort(value: String) {
|
||||
preferences.edit { putString(keyFrequencyPort, value) }
|
||||
_rcSettings.update { it.copy(frequencyPort = value) }
|
||||
}
|
||||
|
||||
override fun setFrequencyFormat(value: String) {
|
||||
preferences.edit { putString(keyFrequencyFormat, value) }
|
||||
_rcSettings.update { it.copy(frequencyFormat = value) }
|
||||
}
|
||||
|
||||
private fun getRCSettings(): RCSettings = RCSettings(
|
||||
rotatorState = preferences.getBoolean(keyRotatorState, false),
|
||||
rotatorAddress = preferences.getString(keyRotatorAddress, null) ?: "127.0.0.1",
|
||||
rotatorPort = preferences.getString(keyRotatorPort, null) ?: "4533",
|
||||
rotatorFormat = preferences.getString(keyRotatorFormat, null) ?: $$"P $AZ $EL",
|
||||
frequencyState = preferences.getBoolean(keyFrequencyState, false),
|
||||
frequencyAddress = preferences.getString(keyFrequencyAddress, null) ?: "127.0.0.1",
|
||||
frequencyPort = preferences.getString(keyFrequencyPort, null) ?: "4532",
|
||||
frequencyFormat = preferences.getString(keyFrequencyFormat, null) ?: $$"set_freq $FREQ",
|
||||
bluetoothRotatorState = preferences.getBoolean(keyBluetoothRotatorState, false),
|
||||
bluetoothRotatorFormat = preferences.getString(keyBluetoothRotatorFormat, null) ?: $$"W$AZ $EL",
|
||||
bluetoothRotatorName = preferences.getString(keyBluetoothRotatorName, null) ?: "Default",
|
||||
bluetoothRotatorAddress = preferences.getString(keyBluetoothRotatorAddress, null) ?: "00:0C:BF:13:80:5D",
|
||||
bluetoothFrequencyState = preferences.getBoolean(keyBluetoothFrequencyState, false),
|
||||
bluetoothFrequencyAddress = preferences.getString(keyBluetoothFrequencyAddress, null) ?: "00:0C:BF:13:80:5D",
|
||||
bluetoothFrequencyFormat = preferences.getString(keyBluetoothFrequencyFormat, null) ?: $$"FA$FREQ"
|
||||
)
|
||||
//endregion
|
||||
|
||||
//region # Other settings
|
||||
private val _otherSettings = MutableStateFlow(getOtherSettings())
|
||||
override val otherSettings: StateFlow<OtherSettings> = _otherSettings
|
||||
|
||||
override fun setStateOfAutoUpdate(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyStateOfAutoUpdate, value) }
|
||||
_otherSettings.update { it.copy(stateOfAutoUpdate = value) }
|
||||
}
|
||||
|
||||
override fun setStateOfSensors(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyStateOfSensors, value) }
|
||||
_otherSettings.update { it.copy(stateOfSensors = value) }
|
||||
}
|
||||
|
||||
override fun setStateOfSweep(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyStateOfSweep, value) }
|
||||
_otherSettings.update { it.copy(stateOfSweep = value) }
|
||||
}
|
||||
|
||||
override fun setStateOfUtc(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyStateOfUtc, value) }
|
||||
_otherSettings.update { it.copy(stateOfUtc = value) }
|
||||
}
|
||||
|
||||
override fun setStateOfLightTheme(value: Boolean){
|
||||
preferences.edit { putBoolean(keyStateOfLightTheme, value) }
|
||||
_otherSettings.update { it.copy(stateOfLightTheme = value) }
|
||||
}
|
||||
|
||||
override fun setWarningDismissed() {
|
||||
preferences.edit { putBoolean(keyShouldSeeWarning, false) }
|
||||
_otherSettings.update { it.copy(shouldSeeWarning = false) }
|
||||
}
|
||||
|
||||
override fun setWhatsNewDismissed() {
|
||||
preferences.edit { putBoolean(keyShouldSeeWhatsNew, false) }
|
||||
_otherSettings.update { it.copy(shouldSeeWhatsNew = false) }
|
||||
}
|
||||
|
||||
private fun getOtherSettings(): OtherSettings = OtherSettings(
|
||||
stateOfAutoUpdate = preferences.getBoolean(keyStateOfAutoUpdate, true),
|
||||
stateOfSensors = preferences.getBoolean(keyStateOfSensors, true),
|
||||
stateOfSweep = preferences.getBoolean(keyStateOfSweep, true),
|
||||
stateOfUtc = preferences.getBoolean(keyStateOfUtc, false),
|
||||
stateOfLightTheme = preferences.getBoolean(keyStateOfLightTheme, false),
|
||||
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true),
|
||||
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true)
|
||||
)
|
||||
//endregion
|
||||
|
||||
//region # Data sources settings
|
||||
private val _dataSourcesSettings = MutableStateFlow(getDataSourcesSettings())
|
||||
override val dataSourcesSettings: StateFlow<DataSourcesSettings> = _dataSourcesSettings
|
||||
|
||||
override fun setUseCustomTle(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyUseCustomTle, value) }
|
||||
_dataSourcesSettings.update { it.copy(useCustomTLE = value) }
|
||||
}
|
||||
|
||||
override fun setUseCustomTransceivers(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyUseCustomTransceivers, value) }
|
||||
_dataSourcesSettings.update { it.copy(useCustomTransceivers = value) }
|
||||
}
|
||||
|
||||
override fun setTleUrl(value: String) {
|
||||
preferences.edit { putString(keyTleUrl, value) }
|
||||
_dataSourcesSettings.update { it.copy(tleUrl = value) }
|
||||
}
|
||||
|
||||
override fun setTransceiversUrl(value: String) {
|
||||
preferences.edit { putString(keyTransceiversUrl, value) }
|
||||
_dataSourcesSettings.update { it.copy(transceiversUrl = value) }
|
||||
}
|
||||
|
||||
private fun getDataSourcesSettings(): DataSourcesSettings = DataSourcesSettings(
|
||||
useCustomTLE = preferences.getBoolean(keyUseCustomTle, false),
|
||||
useCustomTransceivers = preferences.getBoolean(keyUseCustomTransceivers, false),
|
||||
tleUrl = preferences.getString(keyTleUrl, "https://example.com/tle.txt") ?: "",
|
||||
transceiversUrl = preferences.getString(keyTransceiversUrl, "https://example.com/radio.json") ?: ""
|
||||
)
|
||||
//endregion
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.source
|
||||
|
||||
import com.rtbishop.look4sat.core.data.database.Look4SatDao
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatEntry as FrameworkEntry
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatRadio as FrameworkRadio
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio as DomainRadio
|
||||
|
||||
class LocalSource(private val look4SatDao: Look4SatDao) : ILocalSource {
|
||||
|
||||
//region # Entries region
|
||||
|
||||
override suspend fun getEntriesTotal() = look4SatDao.getEntriesTotal()
|
||||
|
||||
override suspend fun getEntriesList() = look4SatDao.getEntriesList()
|
||||
|
||||
override suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject> {
|
||||
val selectedOrbitalObjects = mutableListOf<OrbitalObject>()
|
||||
ids.chunked(999).forEach { idsPart ->
|
||||
val entries = look4SatDao.getEntriesWithIds(idsPart)
|
||||
selectedOrbitalObjects.addAll(entries.toDomain().map { entry -> entry.getObject() })
|
||||
}
|
||||
return selectedOrbitalObjects
|
||||
}
|
||||
|
||||
override suspend fun insertEntries(entries: List<OrbitalData>) = look4SatDao.insertEntries(entries.toEntity())
|
||||
|
||||
override suspend fun deleteEntries() = look4SatDao.deleteEntries()
|
||||
|
||||
override suspend fun getIdsWithModes(modes: List<String>) = look4SatDao.getIdsWithModes(modes)
|
||||
|
||||
private fun FrameworkEntry.toDomain() = OrbitalData(
|
||||
this.name, this.epoch, this.meanmo, this.eccn, this.incl,
|
||||
this.raan, this.argper, this.meanan, this.catnum, this.bstar
|
||||
)
|
||||
|
||||
private fun OrbitalData.toEntity() = FrameworkEntry(
|
||||
this.name, this.epoch, this.meanmo, this.eccn, this.incl,
|
||||
this.raan, this.argper, this.meanan, this.catnum, this.bstar
|
||||
)
|
||||
|
||||
private fun List<OrbitalData>.toEntity() = this.map { item -> item.toEntity() }
|
||||
|
||||
private fun List<FrameworkEntry>.toDomain() = this.map { item -> item.toDomain() }
|
||||
|
||||
//endregion
|
||||
|
||||
//region # Radios region
|
||||
|
||||
override suspend fun getRadiosTotal() = look4SatDao.getRadiosTotal()
|
||||
|
||||
override suspend fun getRadiosWithId(id: Int): List<SatRadio> {
|
||||
return look4SatDao.getRadiosWithId(id).toDomainRadios()
|
||||
}
|
||||
|
||||
override suspend fun insertRadios(radios: List<SatRadio>) {
|
||||
look4SatDao.deleteRadios()
|
||||
look4SatDao.insertRadios(radios.toFrameworkRadios())
|
||||
}
|
||||
|
||||
override suspend fun deleteRadios() = look4SatDao.deleteRadios()
|
||||
|
||||
private fun DomainRadio.toFramework() = FrameworkRadio(
|
||||
this.uuid, this.info, this.isAlive, this.downlinkLow, this.downlinkHigh, this.downlinkMode,
|
||||
this.uplinkLow, this.uplinkHigh, this.uplinkMode, this.isInverted, this.catnum
|
||||
)
|
||||
|
||||
private fun FrameworkRadio.toDomain() = DomainRadio(
|
||||
this.uuid, this.info, this.isAlive, this.downlinkLow, this.downlinkHigh, this.downlinkMode,
|
||||
this.uplinkLow, this.uplinkHigh, this.uplinkMode, this.isInverted, this.catnum
|
||||
)
|
||||
|
||||
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
|
||||
|
||||
private fun List<FrameworkRadio>.toDomainRadios() = this.map { radio -> radio.toDomain() }
|
||||
|
||||
//endregion
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.source
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.InputStream
|
||||
|
||||
class RemoteSource(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val httpClient: OkHttpClient
|
||||
) : IRemoteSource {
|
||||
|
||||
override suspend fun getFileStream(uri: String): InputStream? = withContext(dispatcher) {
|
||||
try {
|
||||
val fileUri = uri.toUri()
|
||||
contentResolver.openInputStream(fileUri)?.buffered()
|
||||
} catch (exception: Exception) {
|
||||
println("RemoteSource file stream exception: $exception")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
|
||||
try {
|
||||
val networkRequest = Request.Builder().url(url).build()
|
||||
httpClient.newCall(networkRequest).execute().body.byteStream()
|
||||
} catch (exception: Exception) {
|
||||
println("RemoteSource network stream exception: $exception")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.usecase
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.CalendarContract
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||
|
||||
class AddToCalendar(private val context: Context) : IAddToCalendar {
|
||||
override fun invoke(name: String, aosTime: Long, losTime: Long) {
|
||||
val intent = Intent(Intent.ACTION_INSERT).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setData(CalendarContract.Events.CONTENT_URI)
|
||||
putExtra(CalendarContract.Events.TITLE, name)
|
||||
putExtra(CalendarContract.Events.DESCRIPTION, "Look4Sat Pass")
|
||||
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, aosTime)
|
||||
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, losTime)
|
||||
}
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.usecase
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
|
||||
class ShowToast(private val context: Context) : IShowToast {
|
||||
override fun invoke(message: String) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user