Major classes refactoring and code cleanup

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:33 +00:00
1 parent 6572dd4eab
commit f7f317cf8b
67 files changed
+610 -754

No files matched your search

+2 -2
View File
@@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".presentation.MainApplication"
android:name=".MainApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@@ -24,7 +24,7 @@
android:theme="@style/Theme.Look4Sat.Main"
tools:targetApi="31">
<activity
android:name=".presentation.MainActivity"
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Look4Sat.SplashScreen">
<intent-filter>
@@ -0,0 +1,47 @@
/*
* 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
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.rtbishop.look4sat.presentation.MainScreen
import com.rtbishop.look4sat.presentation.MainTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
installSplashScreen()
super.onCreate(savedInstanceState)
setContent { MainTheme { MainScreen() } }
}
override fun attachBaseContext(newBase: Context?) {
val newConfig = Configuration(newBase?.resources?.configuration)
newConfig.fontScale = 1.0f
applyOverrideConfiguration(newConfig)
super.attachBaseContext(newBase)
}
}
@@ -15,12 +15,11 @@
* 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.presentation
package com.rtbishop.look4sat
import android.app.Application
import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.framework.SettingsManager
import com.rtbishop.look4sat.domain.ISettingsSource
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import java.text.SimpleDateFormat
@@ -31,10 +30,10 @@ import javax.inject.Inject
class MainApplication : Application() {
@Inject
lateinit var repository: IDataRepository
lateinit var dataRepository: IDataRepository
@Inject
lateinit var settingsManager: SettingsManager
lateinit var settingsSource: ISettingsSource
override fun onCreate() {
super.onCreate()
@@ -45,12 +44,12 @@ class MainApplication : Application() {
}
private fun checkAutoUpdate() {
if (settingsManager.isUpdateEnabled()) {
val timeDelta = System.currentTimeMillis() - settingsManager.getLastUpdateTime()
if (settingsSource.isUpdateEnabled()) {
val timeDelta = System.currentTimeMillis() - settingsSource.getLastUpdateTime()
if (timeDelta > 172800000) { // 48 hours in ms
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
Timber.d("Started periodic data update on ${sdf.format(Date())}")
repository.updateFromWeb()
dataRepository.updateFromWeb()
}
}
}
@@ -0,0 +1,102 @@
/*
* 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
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.data.DataParser
import com.rtbishop.look4sat.data.DataRepository
import com.rtbishop.look4sat.domain.*
import com.rtbishop.look4sat.framework.*
import com.rtbishop.look4sat.framework.data.*
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 dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object MainModule {
@Provides
@Singleton
fun provideBluetoothReporter(@ApplicationContext context: Context): BluetoothReporter {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO))
}
@Provides
@Singleton
fun provideNetworkReporter(): NetworkReporter = NetworkReporter(CoroutineScope(Dispatchers.IO))
@Provides
@Singleton
fun provideDataRepository(
@ApplicationContext context: Context, settings: ISettingsSource
): IDataRepository {
val db = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb")
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
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)
return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings)
}
@Provides
@Singleton
fun provideLocationSource(
@ApplicationContext context: Context, settings: ISettingsSource
): ILocationSource {
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return LocationSource(manager, settings)
}
@Provides
@Singleton
fun provideOrientationSource(@ApplicationContext context: Context): OrientationSource {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return OrientationSource(manager, sensor)
}
@Provides
@Singleton
fun provideSatelliteManager(): ISatelliteManager = SatelliteManager(Dispatchers.Default)
@Provides
@Singleton
fun provideSettingsSource(@ApplicationContext context: Context): ISettingsSource {
return SettingsSource(context.getSharedPreferences("default", Context.MODE_PRIVATE))
}
}
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.radarScreen
package com.rtbishop.look4sat.framework
import android.bluetooth.BluetoothManager
import android.util.Log
@@ -9,7 +9,10 @@ import java.io.OutputStream
import java.util.*
import kotlin.math.abs
class BTReporter(private val manager: BluetoothManager, private val reporterScope: CoroutineScope) {
class BluetoothReporter(
private val manager: BluetoothManager,
private val reporterScope: CoroutineScope
) {
private val tag = "BTReporter"
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
@@ -1,139 +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
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationManager @Inject constructor(
@ApplicationContext private val context: Context,
private val manager: LocationManager,
private val settings: ISettingsManager,
) : LocationListener, ILocationManager {
private val providerDef = LocationManager.PASSIVE_PROVIDER
private val providerNet = LocationManager.NETWORK_PROVIDER
private val providerGps = LocationManager.GPS_PROVIDER
private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION
private val _stationPosition = MutableStateFlow<DataState<GeoPos>>(DataState.Handled)
private var currentLocator = settings.loadStationLocator()
private var currentPosition = settings.loadStationPosition()
override val stationPosition: StateFlow<DataState<GeoPos>> = _stationPosition
override fun getStationLocator(): String = currentLocator
override fun getStationPosition(): GeoPos = currentPosition
override fun setStationPosition(latitude: Double, longitude: Double) {
if (QthConverter.isValidPosition(latitude, longitude)) {
val tempLon = if (longitude > 180.0) longitude - 180 else longitude
val newLat = latitude.round(4)
val newLon = tempLon.round(4)
QthConverter.positionToQth(newLat, newLon)?.let { locator ->
currentLocator = locator
currentPosition = GeoPos(newLat, newLon)
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
}
} else {
_stationPosition.value =
DataState.Error(context.getString(R.string.location_manual_error))
}
}
override fun setPositionFromGps() {
val result = ContextCompat.checkSelfPermission(context, locationFine)
if (manager.isProviderEnabled(providerGps) && result == PackageManager.PERMISSION_GRANTED) {
val location = manager.getLastKnownLocation(providerDef)
if (location != null) {
setStationPosition(location.latitude, location.longitude)
} else {
_stationPosition.value = DataState.Loading
manager.requestLocationUpdates(providerGps, 0L, 0f, this)
}
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
}
}
override fun setPositionFromNet() {
val result = ContextCompat.checkSelfPermission(context, locationCoarse)
if (manager.isProviderEnabled(providerNet) && result == PackageManager.PERMISSION_GRANTED) {
val location = manager.getLastKnownLocation(providerDef)
if (location != null) {
setStationPosition(location.latitude, location.longitude)
} else {
_stationPosition.value = DataState.Loading
manager.requestLocationUpdates(providerNet, 0L, 0f, this)
}
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
}
}
override fun setPositionFromQth(locator: String) {
val position = QthConverter.qthToPosition(locator)
if (position != null) {
currentLocator = locator
currentPosition = position
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_qth_error))
}
}
override fun setPositionHandled() {
_stationPosition.value = DataState.Handled
}
override fun onLocationChanged(location: Location) {
manager.removeUpdates(this)
setStationPosition(location.latitude, location.longitude)
}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
}
@@ -0,0 +1,87 @@
/*
* 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
import android.location.Location
import android.location.LocationManager
import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import com.rtbishop.look4sat.domain.ILocationSource
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.StationPos
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
class LocationSource(
private val manager: LocationManager,
private val settings: ISettingsSource,
) : ILocationSource, LocationListenerCompat {
private val _stationPosition = MutableStateFlow(settings.loadStationPosition())
override val stationPosition: StateFlow<StationPos> = _stationPosition
override fun setGpsPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(manager)) return false
forceLocationUpdate(LocationManager.GPS_PROVIDER)
forceLocationUpdate(LocationManager.NETWORK_PROVIDER)
return true
}
override fun setGeoPosition(latitude: Double, longitude: Double): Boolean {
manager.removeUpdates(this)
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
val locator = QthConverter.positionToQth(latitude, newLongitude) ?: return false
saveStationPosition(latitude, newLongitude, locator)
return true
}
override fun setQthPosition(locator: String): Boolean {
val position = QthConverter.qthToPosition(locator) ?: return false
saveStationPosition(position.lat, position.lon, locator)
return true
}
override fun onLocationChanged(location: Location) {
setGeoPosition(location.latitude, location.longitude)
}
private fun forceLocationUpdate(provider: String) {
try {
val location = manager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
if (location == null || System.currentTimeMillis() - location.time > 600000) {
Timber.d("Requesting $provider location update")
manager.requestLocationUpdates(provider, 0L, 0f, this)
} else {
setGeoPosition(location.latitude, location.longitude)
}
} catch (exception: SecurityException) {
Timber.d("No permissions were given")
}
}
private fun saveStationPosition(latitude: Double, longitude: Double, locator: String) {
val newLat = latitude.round(4)
val newLon = longitude.round(4)
Timber.d("Received new Position($newLat, $newLon) & Locator $locator")
_stationPosition.value = StationPos(newLat, newLon, locator, System.currentTimeMillis())
settings.saveStationPosition(stationPosition.value)
}
}
@@ -0,0 +1,70 @@
/*
* 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
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.SocketChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
class NetworkReporter(private val reporterScope: CoroutineScope) {
private var rotationSocketChannel: SocketChannel? = null
private var rotationReporting: Job? = null
// private var frequencySocketChannel: SocketChannel? = null
// private var frequencyReporting: Job? = null
//
// fun reportFrequency(server: String, port: Int, frequency: Long) {
// frequencyReporting = reporterScope.launch {
// runCatching {
// if (frequencySocketChannel == null) {
// frequencySocketChannel = SocketChannel.open(InetSocketAddress(server, port))
// } else {
// val buffer = ByteBuffer.wrap("\\set_freq $frequency\n".toByteArray())
// frequencySocketChannel?.write(buffer)
// }
// }.onFailure { error ->
// println(error.localizedMessage)
// frequencySocketChannel = null
// frequencyReporting?.cancelAndJoin()
// }
// }
// }
fun reportRotation(server: String, port: Int, azimuth: Double, elevation: Double) {
rotationReporting = reporterScope.launch {
val newElevation = if (elevation > 0.0) elevation else 0.0
runCatching {
if (rotationSocketChannel == null) {
rotationSocketChannel = SocketChannel.open(InetSocketAddress(server, port))
} else {
val buffer = ByteBuffer.wrap("\\P $azimuth $newElevation\n".toByteArray())
rotationSocketChannel?.write(buffer)
}
}.onFailure { error ->
println(error.localizedMessage)
rotationSocketChannel = null
rotationReporting?.cancelAndJoin()
}
}
}
}
@@ -21,47 +21,36 @@ import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import com.rtbishop.look4sat.domain.predict.RAD2DEG
import javax.inject.Inject
import javax.inject.Singleton
import com.rtbishop.look4sat.domain.RAD2DEG
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlin.math.round
@Singleton
class OrientationManager @Inject constructor(private val sensorManager: SensorManager) :
SensorEventListener {
class OrientationSource(
private val manager: SensorManager, private val sensor: Sensor?
) : SensorEventListener {
interface OrientationListener {
fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float)
}
private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
private val rotationMatrix = FloatArray(9)
private val orientationValues = FloatArray(3)
private var sensorAccuracy: Int = SensorManager.SENSOR_STATUS_UNRELIABLE
private var orientationListener: OrientationListener? = null
fun startListening(listener: OrientationListener) {
if (sensor != null && orientationListener !== listener) {
orientationListener = listener
sensorManager.registerListener(this, sensor, 16000)
} else return
private val _orientation = MutableStateFlow(Pair(0f, 0f))
val orientation: StateFlow<Pair<Float, Float>> = _orientation
fun startListening() = sensor?.let {
manager.registerListener(this, it, 16000)
}
fun stopListening() {
sensorManager.unregisterListener(this)
orientationListener = null
}
fun stopListening() = manager.unregisterListener(this)
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
sensorAccuracy = accuracy
}
override fun onSensorChanged(event: SensorEvent) {
when {
orientationListener == null -> return
sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> return
event.sensor == sensor -> updateOrientation(event.values)
}
override fun onSensorChanged(event: SensorEvent) = when {
sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> {}
event.sensor == sensor -> updateOrientation(event.values)
else -> {}
}
private fun updateOrientation(rotationVector: FloatArray) {
@@ -69,11 +58,11 @@ class OrientationManager @Inject constructor(private val sensorManager: SensorMa
SensorManager.getOrientation(rotationMatrix, orientationValues)
val azimuth = (orientationValues[0] * RAD2DEG).toFloat()
val pitch = (orientationValues[1] * RAD2DEG).toFloat()
val roll = (orientationValues[2] * RAD2DEG).toFloat()
// val roll = (orientationValues[2] * RAD2DEG).toFloat()
val magneticAzimuth = (azimuth + 360f) % 360f
val roundedAzimuth = round(magneticAzimuth * 10) / 10
val roundedPitch = round(pitch * 10) / 10
val roundedRoll = round(roll * 10) / 10
orientationListener?.onOrientationChanged(roundedAzimuth, roundedPitch, roundedRoll)
// val roundedRoll = round(roll * 10) / 10
_orientation.value = Pair(roundedAzimuth, roundedPitch)
}
}
@@ -19,13 +19,10 @@ package com.rtbishop.look4sat.framework
import android.content.SharedPreferences
import androidx.core.content.edit
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.predict.GeoPos
import javax.inject.Inject
import javax.inject.Singleton
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.StationPos
@Singleton
class SettingsManager @Inject constructor(private val prefs: SharedPreferences) : ISettingsManager {
class SettingsSource(private val prefs: SharedPreferences) : ISettingsSource {
companion object {
const val keyModes = "satModes"
@@ -46,29 +43,23 @@ class SettingsManager @Inject constructor(private val prefs: SharedPreferences)
const val keyLatitude = "stationLat"
const val keyLongitude = "stationLon"
const val keyLocator = "stationQTH"
const val keyLocTimestamp = "locTimestamp"
const val keySelection = "selection"
}
override fun loadStationLocator(): String {
return prefs.getString(keyLocator, null) ?: ""
override fun loadStationPosition(): StationPos {
val latitude = prefs.getString(keyLatitude, null) ?: "0.0"
val longitude = prefs.getString(keyLongitude, null) ?: "0.0"
val qthLocator = prefs.getString(keyLocator, null) ?: ""
val timestamp = prefs.getLong(keyLocTimestamp, 0L)
return StationPos(latitude.toDouble(), longitude.toDouble(), qthLocator, timestamp)
}
override fun saveStationLocator(locator: String) {
prefs.edit { putString(keyLocator, locator) }
}
override fun loadStationPosition(): GeoPos {
val defaultSP = "0.0"
val latitude = prefs.getString(keyLatitude, null) ?: defaultSP
val longitude = prefs.getString(keyLongitude, null) ?: defaultSP
return GeoPos(latitude.toDouble(), longitude.toDouble())
}
override fun saveStationPosition(position: GeoPos) {
prefs.edit {
putString(keyLatitude, position.lat.toString())
putString(keyLongitude, position.lon.toString())
}
override fun saveStationPosition(stationPos: StationPos) = prefs.edit {
putString(keyLatitude, stationPos.latitude.toString())
putString(keyLongitude, stationPos.longitude.toString())
putString(keyLocator, stationPos.qthLocator)
putLong(keyLocTimestamp, stationPos.timestamp)
}
override fun saveEntriesSelection(catnums: List<Int>) {
@@ -27,7 +27,7 @@ import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatRadio
@Database(entities = [SatEntry::class, SatRadio::class], version = 2, exportSchema = true)
abstract class LocalDatabase : RoomDatabase() {
abstract class MainDatabase : RoomDatabase() {
abstract fun entriesDao(): EntriesDao
@@ -15,17 +15,17 @@
* 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
package com.rtbishop.look4sat.framework.data.local
import com.rtbishop.look4sat.domain.data.ILocalEntrySource
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.data.IEntriesStorage
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.framework.data.dao.EntriesDao
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem
import com.rtbishop.look4sat.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.model.SatItem as DomainItem
class LocalEntrySource(private val entriesDao: EntriesDao) : ILocalEntrySource {
class EntriesStorage(private val entriesDao: EntriesDao) : IEntriesStorage {
override fun getEntriesTotal() = entriesDao.getEntriesTotal()
@@ -15,14 +15,14 @@
* 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
package com.rtbishop.look4sat.framework.data.local
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.domain.data.ILocalRadioSource
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 LocalRadioSource(private val radiosDao: RadiosDao) : ILocalRadioSource {
class RadiosStorage(private val radiosDao: RadiosDao) : IRadiosStorage {
override fun getRadiosTotal() = radiosDao.getRadiosTotal()
@@ -31,7 +31,6 @@ class LocalRadioSource(private val radiosDao: RadiosDao) : ILocalRadioSource {
}
override suspend fun insertRadios(radios: List<DomainRadio>) {
radiosDao.deleteRadios()
radiosDao.insertRadios(radios.toFrameworkRadios())
}
@@ -15,22 +15,21 @@
* 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
package com.rtbishop.look4sat.framework.data.remote
import android.content.ContentResolver
import android.net.Uri
import com.rtbishop.look4sat.domain.data.IFileDataSource
import com.rtbishop.look4sat.data.IFileSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.io.InputStream
class FileDataSource(
class FileSource(
private val contentResolver: ContentResolver,
private val ioDispatcher: CoroutineDispatcher
) : IFileDataSource {
private val coroutineDispatcher: CoroutineDispatcher
) : IFileSource {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getDataStream(uri: String): InputStream? {
return withContext(ioDispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
return withContext(coroutineDispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
}
}
@@ -15,25 +15,17 @@
* 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
package com.rtbishop.look4sat.framework.data.remote
import com.rtbishop.look4sat.domain.data.IRemoteDataSource
import com.rtbishop.look4sat.data.INetworkSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.InputStream
import java.net.URL
class RemoteDataSource(
private val httpClient: OkHttpClient, private val ioDispatcher: CoroutineDispatcher
) : IRemoteDataSource {
class NetworkSource(private val coroutineDispatcher: CoroutineDispatcher) : INetworkSource {
override suspend fun getDataStream(url: String): InputStream? = withContext(ioDispatcher) {
try {
val request = Request.Builder().url(url).build()
httpClient.newCall(request).execute().body?.byteStream()
} catch (exception: Exception) {
null
}
override suspend fun getDataStream(url: String): InputStream? {
return withContext(coroutineDispatcher) { URL(url).openStream() }
}
}
@@ -19,7 +19,7 @@ package com.rtbishop.look4sat.framework.model
import androidx.room.Embedded
import androidx.room.Entity
import com.rtbishop.look4sat.domain.predict.OrbitalData
import com.rtbishop.look4sat.model.OrbitalData
@Entity(tableName = "entries", primaryKeys = ["catnum"])
data class SatEntry(@Embedded val data: OrbitalData, var comment: String? = null)
@@ -1,59 +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.injection
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.SharedPreferences
import android.hardware.SensorManager
import android.location.LocationManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideBluetoothManager(@ApplicationContext context: Context): BluetoothManager {
return context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
}
@Provides
@Singleton
fun provideLocationManager(@ApplicationContext context: Context): LocationManager {
return context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
@Provides
@Singleton
fun provideSensorManager(@ApplicationContext context: Context): SensorManager {
return context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
@Provides
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("default", Context.MODE_PRIVATE)
}
}
@@ -1,93 +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.injection
import android.bluetooth.BluetoothManager
import android.content.Context
import androidx.room.Room
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.data.DataRepository
import com.rtbishop.look4sat.domain.predict.SatelliteManager
import com.rtbishop.look4sat.framework.LocationManager
import com.rtbishop.look4sat.framework.SettingsManager
import com.rtbishop.look4sat.framework.data.FileDataSource
import com.rtbishop.look4sat.framework.data.LocalDatabase
import com.rtbishop.look4sat.framework.data.LocalEntrySource
import com.rtbishop.look4sat.framework.data.LocalRadioSource
import com.rtbishop.look4sat.framework.data.MIGRATION_1_2
import com.rtbishop.look4sat.framework.data.RemoteDataSource
import com.rtbishop.look4sat.presentation.radarScreen.BTReporter
import com.rtbishop.look4sat.utility.DataParser
import com.rtbishop.look4sat.utility.DataReporter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import okhttp3.OkHttpClient
@Module
@InstallIn(SingletonComponent::class)
object BaseModule {
@Provides
@Singleton
fun provideDataRepository(
@ApplicationContext context: Context,
settings: ISettingsManager
): IDataRepository {
val db = Room.databaseBuilder(context, LocalDatabase::class.java, "Look4SatDb")
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
val parser = DataParser(Dispatchers.Default)
val fileSource = FileDataSource(context.contentResolver, Dispatchers.IO)
val entries = LocalEntrySource(db.entriesDao())
val radios = LocalRadioSource(db.radiosDao())
val remoteSource = RemoteDataSource(OkHttpClient.Builder().build(), Dispatchers.IO)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings)
}
@Provides
@Singleton
fun provideDataReporter(): DataReporter = DataReporter(CoroutineScope(Dispatchers.IO))
@Provides
@Singleton
fun provideBTReporter(manager: BluetoothManager): BTReporter {
return BTReporter(manager, CoroutineScope(Dispatchers.IO))
}
@Provides
@Singleton
fun provideLocationManager(manager: LocationManager): ILocationManager = manager
@Provides
@Singleton
fun provideSatelliteManager(): ISatelliteManager = SatelliteManager(Dispatchers.Default)
@Provides
@Singleton
fun provideSettingsManager(manager: SettingsManager): ISettingsManager = manager
}
@@ -1,29 +1,5 @@
/*
* 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.presentation
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -32,7 +8,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -41,31 +16,11 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.entriesScreen.EntriesScreen
import com.rtbishop.look4sat.presentation.mapScreen.MapScreen
import com.rtbishop.look4sat.presentation.passesScreen.PassesScreen
import com.rtbishop.look4sat.presentation.radarScreen.RadarScreen
import com.rtbishop.look4sat.presentation.settingsScreen.SettingsScreen
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
installSplashScreen()
super.onCreate(savedInstanceState)
setContent { MainTheme { MainScreen() } }
}
override fun attachBaseContext(newBase: Context?) {
val newConfig = Configuration(newBase?.resources?.configuration)
newConfig.fontScale = 1.0f
applyOverrideConfiguration(newConfig)
super.attachBaseContext(newBase)
}
}
import com.rtbishop.look4sat.presentation.entries.EntriesScreen
import com.rtbishop.look4sat.presentation.map.MapScreen
import com.rtbishop.look4sat.presentation.passes.PassesScreen
import com.rtbishop.look4sat.presentation.radar.RadarScreen
import com.rtbishop.look4sat.presentation.settings.SettingsScreen
sealed class Screen(var title: String, var icon: Int, var route: String) {
object Entries : Screen("Entries", R.drawable.ic_entries, "entries")
@@ -77,7 +32,7 @@ sealed class Screen(var title: String, var icon: Int, var route: String) {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MainScreen(navController: NavHostController = rememberNavController()) {
fun MainScreen(navController: NavHostController = rememberNavController()) {
Scaffold(bottomBar = { MainNavBar(navController = navController) }) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) { MainNavGraph(navController) }
}
@@ -15,7 +15,7 @@
* 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.presentation.passesScreen
package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.settingsScreen
package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.passesScreen
package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.settingsScreen
package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.entriesScreen
package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.entriesScreen
package com.rtbishop.look4sat.presentation.entries
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@@ -28,11 +28,12 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.SatItem
import com.rtbishop.look4sat.presentation.CardLoadingIndicator
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.dialogs.TypesDialog
import com.rtbishop.look4sat.presentation.onClick
@Composable
@@ -15,15 +15,17 @@
* 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.presentation.entriesScreen
package com.rtbishop.look4sat.presentation.entries
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationSource
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatItem
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
@@ -37,7 +39,8 @@ import javax.inject.Inject
class EntriesViewModel @Inject constructor(
private val satelliteManager: ISatelliteManager,
private val repository: IDataRepository,
private val settings: ISettingsManager
private val settings: ISettingsSource,
private val locationSource: ILocationSource
) : ViewModel() {
private val satType = MutableStateFlow("All")
@@ -78,11 +81,12 @@ class EntriesViewModel @Inject constructor(
settings.saveEntriesSelection(newSelection)
val selectedIds = settings.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds)
val stationPos = settings.loadStationPosition()
val stationPos = locationSource.stationPosition.value
val geoPos = GeoPos(stationPos.latitude, stationPos.longitude)
val timeRef = System.currentTimeMillis()
val hoursAhead = settings.getHoursAhead()
val minElev = settings.getMinElevation()
satelliteManager.calculatePasses(satellites, stationPos, timeRef, hoursAhead, minElev)
satelliteManager.calculatePasses(satellites, geoPos, timeRef, hoursAhead, minElev)
}
fun updateSelection(catNums: List<Int>, isSelected: Boolean) {
@@ -15,9 +15,9 @@
* 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.presentation.mapScreen
package com.rtbishop.look4sat.presentation.map
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.model.GeoPos
data class MapData(
val catNum: Int,
@@ -1,5 +1,6 @@
package com.rtbishop.look4sat.presentation.mapScreen
package com.rtbishop.look4sat.presentation.map
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
@@ -22,9 +23,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPos
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
@@ -60,7 +61,8 @@ private val labelRect = Rect()
@Composable
fun MapScreen(viewModel: MapViewModel = hiltViewModel()) {
val context = LocalContext.current
Configuration.getInstance().load(context, viewModel.getPreferences())
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
Configuration.getInstance().load(context, preferences)
viewModel.selectDefaultSatellite(-1)
val stationPos = viewModel.stationPos.collectAsState(initial = null)
val positions = viewModel.positions.collectAsState(initial = null)
@@ -15,17 +15,13 @@
* 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.presentation.mapScreen
package com.rtbishop.look4sat.presentation.map
import android.content.SharedPreferences
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.*
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.toDegrees
import com.rtbishop.look4sat.utility.toTimerString
@@ -42,13 +38,14 @@ import kotlin.math.min
@HiltViewModel
class MapViewModel @Inject constructor(
private val preferences: SharedPreferences,
private val repository: IDataRepository,
private val satelliteManager: ISatelliteManager,
private val settings: ISettingsManager
private val settings: ISettingsSource,
private val locationSource: ILocationSource
) : ViewModel() {
private val stationPosition = settings.loadStationPosition()
private val stationPosNew = locationSource.stationPosition.value
private val stationPosition = GeoPos(stationPosNew.latitude, stationPosNew.longitude)
private var allPasses = satelliteManager.getPasses()
private var allSatellites = listOf<Satellite>()
private var dataUpdateJob: Job? = null
@@ -73,8 +70,6 @@ class MapViewModel @Inject constructor(
private val _positions = MutableSharedFlow<Map<Satellite, GeoPos>>()
val positions: SharedFlow<Map<Satellite, GeoPos>> = _positions
fun getPreferences() = preferences
fun scrollSelection(decrement: Boolean) {
if (allSatellites.isNotEmpty()) {
val index = allSatellites.indexOf(selectedSatellite)
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.passesScreen
package com.rtbishop.look4sat.presentation.passes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@@ -23,12 +23,12 @@ import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.NearEarthSat
import com.rtbishop.look4sat.domain.predict.OrbitalData
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.domain.NearEarthSat
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.OrbitalData
import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.presentation.*
import com.rtbishop.look4sat.presentation.dialogs.FilterDialog
import com.rtbishop.look4sat.presentation.pullRefresh.PullRefreshIndicator
import com.rtbishop.look4sat.presentation.pullRefresh.PullRefreshState
import com.rtbishop.look4sat.presentation.pullRefresh.pullRefresh
@@ -15,15 +15,17 @@
* 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.presentation.passesScreen
package com.rtbishop.look4sat.presentation.passes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationSource
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
@@ -35,7 +37,8 @@ import javax.inject.Inject
class PassesViewModel @Inject constructor(
private val satelliteManager: ISatelliteManager,
private val repository: IDataRepository,
private val settings: ISettingsManager
private val settings: ISettingsSource,
private val locationSource: ILocationSource
) : ViewModel() {
private val _passes = MutableStateFlow<DataState<List<SatPass>>>(DataState.Loading)
@@ -105,11 +108,12 @@ class PassesViewModel @Inject constructor(
selection?.let { items -> settings.saveEntriesSelection(items) }
settings.setHoursAhead(hoursAhead)
settings.setMinElevation(minElevation)
val stationPos = settings.loadStationPosition()
val stationPos = locationSource.stationPosition.value
val geoPos = GeoPos(stationPos.latitude, stationPos.longitude)
val selectedIds = settings.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds)
satelliteManager.calculatePasses(
satellites, stationPos, timeRef, hoursAhead, minElevation
satellites, geoPos, timeRef, hoursAhead, minElevation
)
}
}
@@ -15,8 +15,8 @@
* 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.presentation.radarScreen
package com.rtbishop.look4sat.presentation.radar
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.model.SatPos
data class RadarData(val satPos: SatPos, val satTrack: List<SatPos> = emptyList())
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.radarScreen
package com.rtbishop.look4sat.presentation.radar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -18,15 +18,12 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.model.SatRadio
@Composable
fun RadarScreen(navController: NavController) {
val viewModel: RadarViewModel = hiltViewModel()
val currentPass = viewModel.getPass().collectAsState(null)
val radarData = viewModel.radarData.collectAsState()
val transmitters = viewModel.transmitters.collectAsState()
val orientation = viewModel.orientation.collectAsState()
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
ElevatedCard(modifier = Modifier.height(52.dp)) {
@@ -62,20 +59,18 @@ fun RadarScreen(navController: NavController) {
ElevatedCard(modifier = Modifier
.fillMaxSize()
.weight(1f)) {
radarData.value?.let { data ->
orientation.value?.let { triple ->
RadarViewCompose(
item = data.satPos,
items = data.satTrack,
orientation = triple
)
}
viewModel.radarData.value?.let { data ->
RadarViewCompose(
item = data.satPos,
items = data.satTrack,
azimElev = viewModel.orientation.value
)
}
}
ElevatedCard(modifier = Modifier
.fillMaxSize()
.weight(1f)) {
TransmittersList(transmitters = transmitters.value)
TransmittersList(transmitters = viewModel.transmitters.value)
}
}
}
@@ -161,15 +156,9 @@ private fun TransmitterItem(radio: SatRadio) {
// setShowAim(viewModel.getUseCompass())
// setScanning(viewModel.getShowSweep())
// }
// radarCard.addView(radarView)
// viewModel.radarData.observe(viewLifecycleOwner) { passData ->
// radarView?.setPosition(passData.satPos)
// radarView?.setPositions(passData.satTrack)
// setPassText(pass, passData.satPos)
// }
// viewModel.orientation.observe(viewLifecycleOwner) { value ->
// radarView?.setOrientation(value.first, value.second, value.third)
// }
// }
// }
//}
@@ -1,6 +1,6 @@
@file:OptIn(ExperimentalTextApi::class)
package com.rtbishop.look4sat.presentation.radarScreen
package com.rtbishop.look4sat.presentation.radar
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
@@ -14,14 +14,14 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.text.*
import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.domain.predict.PI_2
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.PI_2
import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.utility.toRadians
import kotlin.math.cos
import kotlin.math.sin
@Composable
fun RadarViewCompose(item: SatPos, items: List<SatPos>, orientation: Triple<Float, Float, Float>) {
fun RadarViewCompose(item: SatPos, items: List<SatPos>, azimElev: Pair<Float, Float>) {
val radarColor = Color(0xFFDCDCDC)
val primaryColor = Color(0xFFFFE082)
val secondaryColor = Color(0xFFDC0000)
@@ -41,14 +41,14 @@ fun RadarViewCompose(item: SatPos, items: List<SatPos>, orientation: Triple<Floa
trackEffect.value = createTrackEffect(trackPath.value)
trackCreated.value = true
}
rotate(-orientation.first) {
rotate(-azimElev.first) {
drawSweep(center, sweepDegrees.value, radius, primaryColor)
drawRadar(radius, radarColor, strokeWidth, 3)
drawInfo(radius, primaryColor, measurer, 3)
translate(center.x, center.y) {
drawTrack(trackPath.value, trackEffect.value, secondaryColor, primaryColor)
if (item.elevation > 0) drawPosition(item, radius, animScale.value, primaryColor)
drawAim(orientation.first, orientation.second, radius, strokeWidth, secondaryColor)
drawAim(azimElev.first, azimElev.second, radius, strokeWidth, secondaryColor)
}
sweepDegrees.value = (sweepDegrees.value + 360 / 12.0f / 60) % 360
}
@@ -15,29 +15,26 @@
* 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.presentation.radarScreen
package com.rtbishop.look4sat.presentation.radar
import android.hardware.GeomagneticField
import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.framework.OrientationManager
import com.rtbishop.look4sat.utility.DataReporter
import com.rtbishop.look4sat.domain.*
import com.rtbishop.look4sat.framework.BluetoothReporter
import com.rtbishop.look4sat.framework.NetworkReporter
import com.rtbishop.look4sat.framework.OrientationSource
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.model.SatRadio
import com.rtbishop.look4sat.utility.round
import com.rtbishop.look4sat.utility.toDegrees
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -46,28 +43,34 @@ import javax.inject.Inject
@HiltViewModel
class RadarViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val orientationManager: OrientationManager,
private val reporter: DataReporter,
private val btReporter: BTReporter,
private val orientationSource: OrientationSource,
private val reporter: NetworkReporter,
private val bluetoothReporter: BluetoothReporter,
private val satManager: ISatelliteManager,
private val repository: IDataRepository,
private val settings: ISettingsManager
) : ViewModel(), OrientationManager.OrientationListener {
private val settings: ISettingsSource,
private val locationSource: ILocationSource
) : ViewModel() {
private val stationPos = settings.loadStationPosition()
private val _passData = MutableStateFlow<RadarData?>(null)
private val _transmitters = MutableStateFlow<List<SatRadio>>(emptyList())
private val _orientation = MutableStateFlow<Triple<Float, Float, Float>?>(null)
val radarData: StateFlow<RadarData?> = _passData
val transmitters: StateFlow<List<SatRadio>> = _transmitters
val orientation: StateFlow<Triple<Float, Float, Float>?> = _orientation
private val stationPos = locationSource.stationPosition.value.toGeoPos()
private val magDeclination = getMagDeclination(stationPos)
val transmitters = mutableStateOf<List<SatRadio>>(emptyList())
val radarData = mutableStateOf<RadarData?>(null)
val orientation = mutableStateOf(orientationSource.orientation.value)
init {
enableSensor()
if (settings.isSensorEnabled()) {
viewModelScope.launch {
orientationSource.startListening()
orientationSource.orientation.collect { data ->
orientation.value = Pair(data.first + magDeclination, data.second)
}
}
}
}
override fun onCleared() {
disableSensor()
orientationSource.stopListening()
super.onCleared()
}
@@ -94,22 +97,10 @@ class RadarViewModel @Inject constructor(
}
}
fun enableSensor() {
if (settings.isSensorEnabled()) orientationManager.startListening(this)
}
fun disableSensor() {
if (settings.isSensorEnabled()) orientationManager.stopListening()
}
fun getUseCompass(): Boolean = settings.isSensorEnabled()
fun getShowSweep(): Boolean = settings.isSweepEnabled()
override fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float) {
_orientation.value = Triple(azimuth + getMagDeclination(stationPos), pitch, roll)
}
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.lat.toFloat()
val longitude = geoPos.lon.toFloat()
@@ -129,7 +120,7 @@ class RadarViewModel @Inject constructor(
val elevation = satPos.elevation.toDegrees().round(1)
reporter.reportRotation(server, port, azimuth, elevation)
}
_passData.value = RadarData(satPos, track)
radarData.value = RadarData(satPos, track)
}
}
@@ -137,14 +128,14 @@ class RadarViewModel @Inject constructor(
viewModelScope.launch {
if (settings.getBTEnabled()) {
val btDevice = settings.getBTDeviceAddr()
if (btReporter.isConnected()) {
if (bluetoothReporter.isConnected()) {
val format = settings.getBTFormat()
val azimuth = satPos.azimuth.toDegrees().round(0).toInt()
val elevation = satPos.elevation.toDegrees().round(0).toInt()
btReporter.reportRotation(format, azimuth, elevation)
} else if (!btReporter.isConnecting()) {
bluetoothReporter.reportRotation(format, azimuth, elevation)
} else if (!bluetoothReporter.isConnecting()) {
Log.i("BTReporter", "BTReporter: Attempting to connect...")
btReporter.connectBTDevice(btDevice)
bluetoothReporter.connectBTDevice(btDevice)
}
}
}
@@ -154,7 +145,7 @@ class RadarViewModel @Inject constructor(
viewModelScope.launch {
delay(125)
val list = satManager.processRadios(satellite, stationPos, radios, time)
_transmitters.value = list
transmitters.value = list
}
}
}
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.settingsScreen
package com.rtbishop.look4sat.presentation.settings
import android.Manifest
import android.os.Build
@@ -26,6 +26,8 @@ import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.CardButton
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.dialogs.LocatorDialog
import com.rtbishop.look4sat.presentation.dialogs.PositionDialog
import com.rtbishop.look4sat.presentation.gotoUrl
import com.rtbishop.look4sat.presentation.showToast
@@ -57,8 +59,8 @@ fun SettingsScreen(navController: NavController) {
val locationPermFine = Manifest.permission.ACCESS_FINE_LOCATION
val locationRequest = rememberLauncherForActivityResult(locationContract) { permissions ->
when {
permissions[locationPermFine] == true -> viewModel.locationSettings.value.setGpsLoc
permissions[locationPermCoarse] == true -> viewModel.locationSettings.value.setGpsLoc
permissions[locationPermFine] == true -> viewModel.locationSettings.value.setGpsLoc()
permissions[locationPermCoarse] == true -> viewModel.locationSettings.value.setGpsLoc()
else -> showToast(context, locationError)
}
}
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.settingsScreen
package com.rtbishop.look4sat.presentation.settings
data class LocationSettings(
val getUpdating: Boolean,
@@ -15,15 +15,15 @@
* 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.presentation.settingsScreen
package com.rtbishop.look4sat.presentation.settings
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.ILocationSource
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.DataState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@@ -31,21 +31,20 @@ import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val locationManager: ILocationManager,
private val locationSource: ILocationSource,
private val repository: IDataRepository,
private val settings: ISettingsManager
private val settings: ISettingsSource
) : ViewModel() {
private val defaultLocationSettings = LocationSettings(false,
settings.getLastUpdateTime(),
locationManager.getStationPosition().lat,
locationManager.getStationPosition().lon,
settings.loadStationLocator(),
{ locationManager.setPositionFromGps() },
{ lat, lon -> locationManager.setStationPosition(lat, lon) },
{ locationManager.setPositionFromQth(it) })
var locationSettings = mutableStateOf(defaultLocationSettings)
private set
locationSource.stationPosition.value.timestamp,
locationSource.stationPosition.value.latitude,
locationSource.stationPosition.value.longitude,
locationSource.stationPosition.value.qthLocator,
{ locationSource.setGpsPosition() },
{ lat, lon -> locationSource.setGeoPosition(lat, lon) },
{ locationSource.setQthPosition(it) })
val locationSettings = mutableStateOf(defaultLocationSettings)
private val defaultDataSettings = DataSettings(false,
settings.getLastUpdateTime(),
@@ -54,8 +53,7 @@ class SettingsViewModel @Inject constructor(
{ repository.updateFromWeb() },
{ repository.updateFromFile(it) },
{ repository.clearAllData() })
var dataSettings = mutableStateOf(defaultDataSettings)
private set
val dataSettings = mutableStateOf(defaultDataSettings)
private val defaultOtherSettings = OtherSettings(settings.isUtcEnabled(),
settings.isUpdateEnabled(),
@@ -65,8 +63,7 @@ class SettingsViewModel @Inject constructor(
{ setUpdate(it) },
{ setSweep(it) },
{ setSensor(it) })
var otherSettings = mutableStateOf(defaultOtherSettings)
private set
val otherSettings = mutableStateOf(defaultOtherSettings)
init {
viewModelScope.launch {
@@ -81,9 +78,7 @@ class SettingsViewModel @Inject constructor(
combine(repository.getEntriesTotal(), repository.getRadiosTotal()) { sats, radios ->
dataSettings.value =
dataSettings.value.copy(getSatellites = sats, getRadios = radios)
}.collect {
}
}.collect {}
}
}