Added various minor fixes, cleaned up classes

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:45 +00:00
1 parent 5a8108384f
commit fc0b995bac
35 files changed
+295 -310

No files matched your search

+4 -7
View File
@@ -1,11 +1,13 @@
@file: Suppress("UnstableApiUsage")
plugins { plugins {
id("com.android.application") id("com.android.application")
id("com.google.devtools.ksp") id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
} }
kotlin {
jvmToolchain(17)
}
android { android {
namespace = "com.rtbishop.look4sat" namespace = "com.rtbishop.look4sat"
compileSdk = 33 compileSdk = 33
@@ -29,12 +31,7 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
} }
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
composeOptions { kotlinCompilerExtensionVersion = "1.5.1" } composeOptions { kotlinCompilerExtensionVersion = "1.5.1" }
packaging { resources { excludes += listOf("META-INF/{AL2.0,LGPL2.1}") } }
} }
dependencies { dependencies {
@@ -40,7 +40,7 @@ class MainApplication : Application() {
if (timeDelta > 172800000) { // 48 hours in ms if (timeDelta > 172800000) { // 48 hours in ms
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault()) val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
println("Started periodic data update on ${sdf.format(Date())}") println("Started periodic data update on ${sdf.format(Date())}")
container.databaseRepo.updateFromWeb() container.databaseRepo.updateFromRemote()
} }
} }
} }
@@ -10,18 +10,21 @@ import com.rtbishop.look4sat.data.database.MIGRATION_1_2
import com.rtbishop.look4sat.data.database.MainDatabase import com.rtbishop.look4sat.data.database.MainDatabase
import com.rtbishop.look4sat.data.framework.BluetoothReporter import com.rtbishop.look4sat.data.framework.BluetoothReporter
import com.rtbishop.look4sat.data.framework.NetworkReporter import com.rtbishop.look4sat.data.framework.NetworkReporter
import com.rtbishop.look4sat.data.framework.SettingsRepo
import com.rtbishop.look4sat.data.repository.DatabaseRepo import com.rtbishop.look4sat.data.repository.DatabaseRepo
import com.rtbishop.look4sat.data.repository.SatelliteRepo import com.rtbishop.look4sat.data.repository.SatelliteRepo
import com.rtbishop.look4sat.data.repository.SelectionRepo import com.rtbishop.look4sat.data.repository.SelectionRepo
import com.rtbishop.look4sat.data.source.DataSource import com.rtbishop.look4sat.data.repository.SettingsRepo
import com.rtbishop.look4sat.data.source.LocalStorage import com.rtbishop.look4sat.data.source.FileSource
import com.rtbishop.look4sat.data.source.LocalSource
import com.rtbishop.look4sat.data.source.RemoteSource
import com.rtbishop.look4sat.data.source.SensorSource import com.rtbishop.look4sat.data.source.SensorSource
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISelectionRepo import com.rtbishop.look4sat.domain.repository.ISelectionRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.IDataSource import com.rtbishop.look4sat.domain.source.IFileSource
import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.source.IRemoteSource
import com.rtbishop.look4sat.domain.source.ISensorSource import com.rtbishop.look4sat.domain.source.ISensorSource
import com.rtbishop.look4sat.domain.utility.DataParser import com.rtbishop.look4sat.domain.utility.DataParser
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
@@ -34,21 +37,16 @@ import org.osmdroid.config.Configuration
class MainContainer(private val context: Context) { class MainContainer(private val context: Context) {
private val database = private val localSource = provideLocalSource()
Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb").addMigrations(MIGRATION_1_2) val mainScope = provideMainScope()
.fallbackToDestructiveMigration().build()
private val localStorage = LocalStorage(database.storageDao())
private val mainHandler = CoroutineExceptionHandler { _, error -> println("Look4Sat: $error") }
private val remoteSource = provideRemoteSource()
val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
val settingsRepo = provideSettingsRepo() val settingsRepo = provideSettingsRepo()
val databaseRepo = provideDatabaseRepo()
val satelliteRepo = provideSatelliteRepo()
val selectionRepo = provideSelectionRepo() val selectionRepo = provideSelectionRepo()
val satelliteRepo = provideSatelliteRepo()
val databaseRepo = provideDatabaseRepo()
fun provideBluetoothReporter(): BluetoothReporter { fun provideBluetoothReporter(): BluetoothReporter {
val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
return BluetoothReporter(btManager, CoroutineScope(Dispatchers.IO)) return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO))
} }
fun provideNetworkReporter(): NetworkReporter { fun provideNetworkReporter(): NetworkReporter {
@@ -56,34 +54,53 @@ class MainContainer(private val context: Context) {
} }
fun provideSensorSource(): ISensorSource { fun provideSensorSource(): ISensorSource {
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return SensorSource(sensorManager, sensor) return SensorSource(manager, sensor)
}
private fun provideRemoteSource(): IDataSource {
val cache = Cache(context.cacheDir, 1000 * 1000 * 10L)
val httpClient = OkHttpClient.Builder().cache(cache).build()
return DataSource(context.contentResolver, httpClient, Dispatchers.IO)
} }
private fun provideDatabaseRepo(): IDatabaseRepo { private fun provideDatabaseRepo(): IDatabaseRepo {
val dataParser = DataParser(Dispatchers.Default) val dataParser = DataParser(Dispatchers.Default)
return DatabaseRepo(Dispatchers.Default, dataParser, remoteSource, localStorage, settingsRepo) val fileSource = provideFileSource()
val remoteSource = provideRemoteSource()
return DatabaseRepo(Dispatchers.Default, dataParser, fileSource, localSource, remoteSource, settingsRepo)
}
private fun provideFileSource(): IFileSource {
return FileSource(context.contentResolver, Dispatchers.IO)
}
private fun provideLocalSource(): ILocalSource {
val database = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb").apply {
addMigrations(MIGRATION_1_2)
fallbackToDestructiveMigration()
}.build()
return LocalSource(database.storageDao())
}
private fun provideMainScope(): CoroutineScope {
val mainHandler = CoroutineExceptionHandler { _, error -> println("Look4Sat: $error") }
return CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
}
private fun provideRemoteSource(): IRemoteSource {
val cache = Cache(context.cacheDir, 1000 * 1000 * 10L)
val httpClient = OkHttpClient.Builder().cache(cache).build()
return RemoteSource(httpClient, Dispatchers.IO)
} }
private fun provideSatelliteRepo(): ISatelliteRepo { private fun provideSatelliteRepo(): ISatelliteRepo {
return SatelliteRepo(Dispatchers.Default, localStorage, settingsRepo) return SatelliteRepo(Dispatchers.Default, localSource, settingsRepo)
} }
private fun provideSelectionRepo(): ISelectionRepo { private fun provideSelectionRepo(): ISelectionRepo {
return SelectionRepo(Dispatchers.Default, localStorage, settingsRepo) return SelectionRepo(Dispatchers.Default, localSource, settingsRepo)
} }
private fun provideSettingsRepo(): ISettingsRepo { private fun provideSettingsRepo(): ISettingsRepo {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE) val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
Configuration.getInstance().load(context, preferences) Configuration.getInstance().load(context, preferences)
return SettingsRepo(locationManager, preferences) return SettingsRepo(manager, preferences)
} }
} }
@@ -16,8 +16,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -34,18 +32,11 @@ private val allModes = listOf(
"LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK", "LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK",
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT" "PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
) )
private val selectedModes = mutableListOf("AFSK", "AFSK S-Net")
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun ModesDialogPreview() { private fun ModesDialogPreview() {
val modesList = remember { mutableStateOf(selectedModes) } MainTheme { ModesDialog(allModes, listOf("AFSK", "AFSK S-Net"), {}) {} }
MainTheme {
ModesDialog(allModes, modesList.value, {}) {
if (modesList.value.contains(it)) modesList.value.remove(it)
else modesList.value.add(it)
}
}
} }
@Composable @Composable
@@ -97,9 +97,7 @@ private fun setStationPosition(stationPos: GeoPos, mapView: MapView) {
} }
} }
private fun setPositions( private fun setPositions(posMap: Map<Satellite, GeoPos>, mapView: MapView, action: (Satellite) -> Unit) {
posMap: Map<Satellite, GeoPos>, mapView: MapView, action: (Satellite) -> Unit
) {
val markers = FolderOverlay() val markers = FolderOverlay()
try { try {
posMap.entries.forEach { posMap.entries.forEach {
@@ -247,9 +245,7 @@ private val tonerLiteTileSource = XYTileSource(
private fun getColorFilter(): ColorMatrixColorFilter { private fun getColorFilter(): ColorMatrixColorFilter {
val negativeMatrix = ColorMatrix( val negativeMatrix = ColorMatrix(
floatArrayOf( floatArrayOf(-1f, 0f, 0f, 0f, 260f, 0f, -1f, 0f, 0f, 260f, 0f, 0f, -1f, 0f, 260f, 0f, 0f, 0f, 1f, 0f)
-1f, 0f, 0f, 0f, 260f, 0f, -1f, 0f, 0f, 260f, 0f, 0f, -1f, 0f, 260f, 0f, 0f, 0f, 1f, 0f
)
) )
return ColorMatrixColorFilter(negativeMatrix) return ColorMatrixColorFilter(negativeMatrix)
} }
@@ -28,9 +28,9 @@ import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.QthConverter
import com.rtbishop.look4sat.domain.utility.clipLat import com.rtbishop.look4sat.domain.utility.clipLat
import com.rtbishop.look4sat.domain.utility.clipLon import com.rtbishop.look4sat.domain.utility.clipLon
import com.rtbishop.look4sat.domain.utility.positionToQth
import com.rtbishop.look4sat.domain.utility.toDegrees import com.rtbishop.look4sat.domain.utility.toDegrees
import com.rtbishop.look4sat.domain.utility.toTimerString import com.rtbishop.look4sat.domain.utility.toTimerString
import java.util.Date import java.util.Date
@@ -66,23 +66,23 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
val footprint: SharedFlow<SatPos> = _footprint val footprint: SharedFlow<SatPos> = _footprint
private val _mapData = MutableSharedFlow<MapData>() private val _mapData = MutableSharedFlow<MapData>()
val mapData: SharedFlow<MapData> = _mapData // val mapData: SharedFlow<MapData> = _mapData
private val _positions = MutableSharedFlow<Map<Satellite, GeoPos>>() private val _positions = MutableSharedFlow<Map<Satellite, GeoPos>>()
val positions: SharedFlow<Map<Satellite, GeoPos>> = _positions val positions: SharedFlow<Map<Satellite, GeoPos>> = _positions
fun scrollSelection(decrement: Boolean) { // fun scrollSelection(decrement: Boolean) {
if (allSatellites.isNotEmpty()) { // if (allSatellites.isNotEmpty()) {
val index = allSatellites.indexOf(selectedSatellite) // val index = allSatellites.indexOf(selectedSatellite)
if (decrement) { // if (decrement) {
if (index > 0) selectSatellite(allSatellites[index - 1]) // if (index > 0) selectSatellite(allSatellites[index - 1])
else selectSatellite(allSatellites[allSatellites.size - 1]) // else selectSatellite(allSatellites[allSatellites.size - 1])
} else { // } else {
if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1]) // if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1])
else selectSatellite(allSatellites[0]) // else selectSatellite(allSatellites[0])
} // }
} // }
} // }
fun selectDefaultSatellite(catnum: Int) { fun selectDefaultSatellite(catnum: Int) {
if (allSatellites.isNotEmpty()) { if (allSatellites.isNotEmpty()) {
@@ -178,7 +178,7 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
val osmLat = clipLat(satPos.latitude.toDegrees()) val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees()) val osmLon = clipLon(satPos.longitude.toDegrees())
val osmPos = GeoPos(osmLat, osmLon) val osmPos = GeoPos(osmLat, osmLon)
val qthLoc = QthConverter.positionToQth(osmPos.latitude, osmPos.longitude) ?: "-- --" val qthLoc = positionToQth(osmPos.latitude, osmPos.longitude) ?: "-- --"
val velocity = satPos.getOrbitalVelocity() val velocity = satPos.getOrbitalVelocity()
val phase = satPos.phase.toDegrees() val phase = satPos.phase.toDegrees()
val visibility = satPos.eclipsed val visibility = satPos.eclipsed
@@ -88,9 +88,7 @@ fun PassesScreen(navToRadar: (Int, Long) -> Unit) {
contentDescription = null contentDescription = null
) )
} }
Column( Column(verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)) {
verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)
) {
Text(text = timerText.value?.first ?: "Null") Text(text = timerText.value?.first ?: "Null")
Text(text = timerText.value?.second ?: "Null") Text(text = timerText.value?.second ?: "Null")
} }
@@ -50,7 +50,7 @@ class PassesViewModel(
fun getFilterSettings() = settingsRepo.passesSettings.value fun getFilterSettings() = settingsRepo.passesSettings.value
fun shouldUseUTC() = settingsRepo.otherSettings.value.utcState // fun shouldUseUTC() = settingsRepo.otherSettings.value.utcState
init { init {
viewModelScope.launch { satelliteRepo.initRepository() } viewModelScope.launch { satelliteRepo.initRepository() }
@@ -73,27 +73,25 @@ fun PullRefreshIndicator(
val showElevation by remember(refreshing, state) { val showElevation by remember(refreshing, state) {
derivedStateOf { refreshing || state.position > 0.5f } derivedStateOf { refreshing || state.position > 0.5f }
} }
Surface( Surface(
modifier = modifier modifier = modifier
.size(IndicatorSize) .size(indicatorSize)
.pullRefreshIndicatorTransform(state, scale), .pullRefreshIndicatorTransform(state, scale),
shape = SpinnerShape, shape = spinnerShape,
color = backgroundColor, color = backgroundColor,
shadowElevation = if (showElevation) Elevation else 0.dp, shadowElevation = if (showElevation) elevation else 0.dp,
) { ) {
Crossfade( Crossfade(
targetState = refreshing, animationSpec = tween(durationMillis = CrossfadeDurationMs) targetState = refreshing,
animationSpec = tween(durationMillis = CROSSFADE_DURATION_MS),
label = "pullToRefresh"
) { refreshing -> ) { refreshing ->
Box( Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center val spinnerSize = (arcRadius + strokeWidth).times(2)
) {
val spinnerSize = (ArcRadius + StrokeWidth).times(2)
if (refreshing) { if (refreshing) {
CircularProgressIndicator( CircularProgressIndicator(
color = contentColor, color = contentColor,
strokeWidth = StrokeWidth, strokeWidth = strokeWidth,
modifier = Modifier.size(spinnerSize), modifier = Modifier.size(spinnerSize),
) )
} else { } else {
@@ -108,18 +106,12 @@ fun PullRefreshIndicator(
* Modifier.size MUST be specified. * Modifier.size MUST be specified.
*/ */
@Composable @Composable
private fun CircularArrowIndicator( private fun CircularArrowIndicator(state: PullRefreshState, color: Color, modifier: Modifier) {
state: PullRefreshState,
color: Color,
modifier: Modifier,
) {
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } } val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
Canvas(modifier.semantics { contentDescription = "Refreshing" }) { Canvas(modifier.semantics { contentDescription = "Refreshing" }) {
val values = ArrowValues(state.progress) val values = ArrowValues(state.progress)
rotate(degrees = values.rotation) { rotate(degrees = values.rotation) {
val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f val arcRadius = arcRadius.toPx() + strokeWidth.toPx() / 2f
val arcBounds = Rect( val arcBounds = Rect(
size.center.x - arcRadius, size.center.x - arcRadius,
size.center.y - arcRadius, size.center.y - arcRadius,
@@ -134,9 +126,7 @@ private fun CircularArrowIndicator(
useCenter = false, useCenter = false,
topLeft = arcBounds.topLeft, topLeft = arcBounds.topLeft,
size = arcBounds.size, size = arcBounds.size,
style = Stroke( style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Square)
width = StrokeWidth.toPx(), cap = StrokeCap.Square
)
) )
drawArrow(path, arcBounds, color, values) drawArrow(path, arcBounds, color, values)
} }
@@ -145,11 +135,7 @@ private fun CircularArrowIndicator(
@Immutable @Immutable
private class ArrowValues( private class ArrowValues(
val alpha: Float, val alpha: Float, val rotation: Float, val startAngle: Float, val endAngle: Float, val scale: Float
val rotation: Float,
val startAngle: Float,
val endAngle: Float,
val scale: Float
) )
private fun ArrowValues(progress: Float): ArrowValues { private fun ArrowValues(progress: Float): ArrowValues {
@@ -164,7 +150,7 @@ private fun ArrowValues(progress: Float): ArrowValues {
// Calculations based on SwipeRefreshLayout specification. // Calculations based on SwipeRefreshLayout specification.
val alpha = progress.coerceIn(0f, 1f) val alpha = progress.coerceIn(0f, 1f)
val endTrim = adjustedPercent * MaxProgressArc val endTrim = adjustedPercent * MAX_PROGRESS_ARC
val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f
val startAngle = rotation * 360 val startAngle = rotation * 360
val endAngle = (rotation + endTrim) * 360 val endAngle = (rotation + endTrim) * 360
@@ -176,33 +162,26 @@ private fun ArrowValues(progress: Float): ArrowValues {
private fun DrawScope.drawArrow(arrow: Path, bounds: Rect, color: Color, values: ArrowValues) { private fun DrawScope.drawArrow(arrow: Path, bounds: Rect, color: Color, values: ArrowValues) {
arrow.reset() arrow.reset()
arrow.moveTo(0f, 0f) // Move to left corner arrow.moveTo(0f, 0f) // Move to left corner
arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner arrow.lineTo(x = arrowWidth.toPx() * values.scale, y = 0f) // Line to right corner
// Line to tip of arrow // Line to tip of arrow
arrow.lineTo( arrow.lineTo(x = arrowWidth.toPx() * values.scale / 2, y = arrowHeight.toPx() * values.scale)
x = ArrowWidth.toPx() * values.scale / 2, y = ArrowHeight.toPx() * values.scale
)
val radius = min(bounds.width, bounds.height) / 2f val radius = min(bounds.width, bounds.height) / 2f
val inset = ArrowWidth.toPx() * values.scale / 2f val inset = arrowWidth.toPx() * values.scale / 2f
arrow.translate( arrow.translate(Offset(x = radius + bounds.center.x - inset, y = bounds.center.y + strokeWidth.toPx() / 2f))
Offset(
x = radius + bounds.center.x - inset, y = bounds.center.y + StrokeWidth.toPx() / 2f
)
)
arrow.close() arrow.close()
rotate(degrees = values.endAngle) { rotate(degrees = values.endAngle) {
drawPath(path = arrow, color = color, alpha = values.alpha) drawPath(path = arrow, color = color, alpha = values.alpha)
} }
} }
private const val CrossfadeDurationMs = 100 private const val CROSSFADE_DURATION_MS = 100
private const val MaxProgressArc = 0.8f private const val MAX_PROGRESS_ARC = 0.8f
private val indicatorSize = 40.dp
private val IndicatorSize = 40.dp private val spinnerShape = CircleShape
private val SpinnerShape = CircleShape private val arcRadius = 7.5.dp
private val ArcRadius = 7.5.dp private val strokeWidth = 2.5.dp
private val StrokeWidth = 2.5.dp private val arrowWidth = 10.dp
private val ArrowWidth = 10.dp private val arrowHeight = 5.dp
private val ArrowHeight = 5.dp private val elevation = 6.dp
private val Elevation = 6.dp
@@ -49,12 +49,10 @@ fun Modifier.pullRefreshIndicatorTransform(
properties["scale"] = scale properties["scale"] = scale
}) { }) {
var height by remember { mutableStateOf(0) } var height by remember { mutableStateOf(0) }
Modifier Modifier
.onSizeChanged { height = it.height } .onSizeChanged { height = it.height }
.graphicsLayer { .graphicsLayer {
translationY = state.position - height translationY = state.position - height
if (scale && !state.refreshing) { if (scale && !state.refreshing) {
val scaleFraction = LinearOutSlowInEasing val scaleFraction = LinearOutSlowInEasing
.transform(state.position / state.threshold) .transform(state.position / state.threshold)
@@ -95,9 +93,7 @@ fun Modifier.pullRefresh(
* [onPull] nor [onRelease] will be invoked. * [onPull] nor [onRelease] will be invoked.
*/ */
fun Modifier.pullRefresh( fun Modifier.pullRefresh(
onPull: (pullDelta: Float) -> Float, onPull: (pullDelta: Float) -> Float, onRelease: suspend (flingVelocity: Float) -> Unit, enabled: Boolean = true
onRelease: suspend (flingVelocity: Float) -> Unit,
enabled: Boolean = true
) = inspectable(inspectorInfo = debugInspectorInfo { ) = inspectable(inspectorInfo = debugInspectorInfo {
name = "pullRefresh" name = "pullRefresh"
properties["onPull"] = onPull properties["onPull"] = onPull
@@ -113,17 +109,13 @@ private class PullRefreshNestedScrollConnection(
private val enabled: Boolean private val enabled: Boolean
) : NestedScrollConnection { ) : NestedScrollConnection {
override fun onPreScroll( override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = when {
available: Offset, source: NestedScrollSource
): Offset = when {
!enabled -> Offset.Zero !enabled -> Offset.Zero
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
else -> Offset.Zero else -> Offset.Zero
} }
override fun onPostScroll( override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = when {
consumed: Offset, available: Offset, source: NestedScrollSource
): Offset = when {
!enabled -> Offset.Zero !enabled -> Offset.Zero
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
else -> Offset.Zero else -> Offset.Zero
@@ -53,27 +53,18 @@ fun rememberPullRefreshState(
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset, refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
): PullRefreshState { ): PullRefreshState {
require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" } require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val onRefreshState = rememberUpdatedState(onRefresh) val onRefreshState = rememberUpdatedState(onRefresh)
val thresholdPx: Float val thresholdPx: Float
val refreshingOffsetPx: Float val refreshingOffsetPx: Float
with(LocalDensity.current) { with(LocalDensity.current) {
thresholdPx = refreshThreshold.toPx() thresholdPx = refreshThreshold.toPx()
refreshingOffsetPx = refreshingOffset.toPx() refreshingOffsetPx = refreshingOffset.toPx()
} }
// refreshThreshold and refreshingOffset should not be changed after instantiation, so any // refreshThreshold and refreshingOffset should not be changed after instantiation, so any
// changes to these values are ignored. // changes to these values are ignored.
val state = remember(scope) { val state = remember(scope) { PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) }
PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) SideEffect { state.setRefreshing(refreshing) }
}
SideEffect {
state.setRefreshing(refreshing)
}
return state return state
} }
@@ -109,7 +100,7 @@ class PullRefreshState internal constructor(
internal val refreshing get() = _refreshing internal val refreshing get() = _refreshing
internal val position get() = _position internal val position get() = _position
private val adjustedDistancePulled by derivedStateOf { distancePulled * DragMultiplier } private val adjustedDistancePulled by derivedStateOf { distancePulled * DRAG_MULTIPLIER }
private var _refreshing by mutableStateOf(false) private var _refreshing by mutableStateOf(false)
private var _position by mutableStateOf(0f) private var _position by mutableStateOf(0f)
@@ -189,4 +180,4 @@ object PullRefreshDefaults {
* the refresh threshold, it is the indicator position, otherwise the indicator position is * the refresh threshold, it is the indicator position, otherwise the indicator position is
* derived from the progress). * derived from the progress).
*/ */
private const val DragMultiplier = 0.5f private const val DRAG_MULTIPLIER = 0.5f
@@ -44,14 +44,9 @@ fun RadarScreen() {
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
IconButton(onClick = {}) { IconButton(onClick = {}) {
Icon( Icon(painter = painterResource(id = R.drawable.ic_filter), contentDescription = null)
painter = painterResource(id = R.drawable.ic_filter),
contentDescription = null
)
} }
Column( Column(verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)) {
verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)
) {
Text(text = "Null") Text(text = "Null")
Text(text = "Null") Text(text = "Null")
} }
@@ -109,10 +104,7 @@ private fun TransmitterItem(radio: SatRadio) {
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.padding(6.dp) .padding(6.dp)
) { ) {
Row( Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_arrow), painter = painterResource(id = R.drawable.ic_arrow),
contentDescription = null, modifier = Modifier.rotate(180f) contentDescription = null, modifier = Modifier.rotate(180f)
@@ -127,10 +119,7 @@ private fun TransmitterItem(radio: SatRadio) {
contentDescription = null contentDescription = null
) )
} }
Row( Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text( Text(
text = stringResource(id = R.string.radio_downlink, radio.downlink ?: 0L), text = stringResource(id = R.string.radio_downlink, radio.downlink ?: 0L),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@@ -43,9 +43,9 @@ fun RadarViewCompose(item: SatPos, items: List<SatPos>, azimElev: Pair<Float, Fl
val primaryColor = Color(0xFFFFE082) val primaryColor = Color(0xFFFFE082)
val secondaryColor = Color(0xFFDC0000) val secondaryColor = Color(0xFFDC0000)
val strokeWidth = 4f val strokeWidth = 4f
val animTransition = rememberInfiniteTransition() val animTransition = rememberInfiniteTransition(label = "animScale")
val animSpec = infiniteRepeatable<Float>(tween(1000)) val animSpec = infiniteRepeatable<Float>(tween(1000))
val animScale = animTransition.animateFloat(16f, 64f, animSpec) val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale")
val measurer = rememberTextMeasurer() val measurer = rememberTextMeasurer()
val sweepDegrees = remember { mutableStateOf(0f) } val sweepDegrees = remember { mutableStateOf(0f) }
val trackCreated = remember { mutableStateOf(false) } val trackCreated = remember { mutableStateOf(false) }
@@ -99,9 +99,9 @@ class RadarViewModel(
} }
} }
fun getUseCompass(): Boolean = settingsRepo.otherSettings.value.sensorState // fun getUseCompass(): Boolean = settingsRepo.otherSettings.value.sensorState
//
fun getShowSweep(): Boolean = settingsRepo.otherSettings.value.sweepState // fun getShowSweep(): Boolean = settingsRepo.otherSettings.value.sweepState
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float { private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.latitude.toFloat() val latitude = geoPos.latitude.toFloat()
@@ -1,7 +1,6 @@
package com.rtbishop.look4sat.presentation.settings package com.rtbishop.look4sat.presentation.settings
import android.Manifest import android.Manifest
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -61,16 +60,16 @@ fun SettingsScreen() {
val context = LocalContext.current val context = LocalContext.current
// Permissions setup // Permissions setup
val bluetoothContract = ActivityResultContracts.RequestPermission() // val bluetoothContract = ActivityResultContracts.RequestPermission()
val bluetoothError = stringResource(R.string.BTremote_perm_error) // val bluetoothError = stringResource(R.string.BTremote_perm_error)
val bluetoothPerm = when { // val bluetoothPerm = when {
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH // Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
else -> Manifest.permission.BLUETOOTH_CONNECT // else -> Manifest.permission.BLUETOOTH_CONNECT
} // }
val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted -> // val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted ->
// viewModel.setBTEnabled(isGranted) // viewModel.setBTEnabled(isGranted)
if (!isGranted) showToast(context, bluetoothError) // if (!isGranted) showToast(context, bluetoothError)
} // }
val locationContract = ActivityResultContracts.RequestMultiplePermissions() val locationContract = ActivityResultContracts.RequestMultiplePermissions()
val locationError = stringResource(R.string.location_gps_error) val locationError = stringResource(R.string.location_gps_error)
val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
@@ -119,9 +118,7 @@ fun SettingsScreen() {
val toggleSensor = { value: Boolean -> viewModel.toggleSensor(value) } val toggleSensor = { value: Boolean -> viewModel.toggleSensor(value) }
// Screen setup // Screen setup
LazyColumn( LazyColumn(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)
) {
item { CardAbout(BuildConfig.VERSION_NAME) } item { CardAbout(BuildConfig.VERSION_NAME) }
item { LocationCard(positionSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos) } item { LocationCard(positionSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos) }
item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) } item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) }
@@ -376,8 +373,7 @@ private fun DataCard(
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun OtherCardPreview() = MainTheme { private fun OtherCardPreview() = MainTheme {
val values = val values = OtherSettings(utcState = false, updateState = true, sweepState = true, sensorState = true)
OtherSettings(utcState = false, updateState = true, sweepState = true, sensorState = true)
OtherCard(settings = values, {}, {}, {}, {}) OtherCard(settings = values, {}, {}, {}, {})
} }
@@ -4,6 +4,4 @@ import com.rtbishop.look4sat.domain.predict.GeoPos
data class PositionSettings(val isUpdating: Boolean, val stationPos: GeoPos, val messageResId: Int) data class PositionSettings(val isUpdating: Boolean, val stationPos: GeoPos, val messageResId: Int)
data class DataSettings( data class DataSettings(val isUpdating: Boolean, val entriesTotal: Int, val radiosTotal: Int, val timestamp: Long)
val isUpdating: Boolean, val entriesTotal: Int, val radiosTotal: Int, val timestamp: Long
)
@@ -106,7 +106,7 @@ class SettingsViewModel(
fun updateFromWeb() = viewModelScope.launch { fun updateFromWeb() = viewModelScope.launch {
try { try {
_dataSettings.value = _dataSettings.value.copy(isUpdating = true) _dataSettings.value = _dataSettings.value.copy(isUpdating = true)
databaseRepo.updateFromWeb() databaseRepo.updateFromRemote()
} catch (exception: Exception) { } catch (exception: Exception) {
_dataSettings.value = _dataSettings.value.copy(isUpdating = false) _dataSettings.value = _dataSettings.value.copy(isUpdating = false)
println(exception.printStackTrace()) println(exception.printStackTrace())
-2
View File
@@ -147,8 +147,6 @@
<string name="data_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string> <string name="data_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string>
<string name="data_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string> <string name="data_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string>
<string name="last_updated_pattern" translatable="false">d MMM yyyy - HH:mm:ss</string>
<string name="remote_title">භ්‍රමක මනාප</string> <string name="remote_title">භ්‍රමක මනාප</string>
<string name="remote_switch">දුරස්ථ පාලනය සබල ක°</string> <string name="remote_switch">දුරස්ථ පාලනය සබල ක°</string>
<string name="remote_ip_hint">IP ලිපිනය</string> <string name="remote_ip_hint">IP ලිපිනය</string>
+4 -6
View File
@@ -1,11 +1,13 @@
@file:Suppress("UnstableApiUsage")
plugins { plugins {
id("com.android.library") id("com.android.library")
id("com.google.devtools.ksp") id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
} }
kotlin {
jvmToolchain(17)
}
android { android {
namespace = "com.rtbishop.look4sat.data" namespace = "com.rtbishop.look4sat.data"
compileSdk = 33 compileSdk = 33
@@ -13,10 +15,6 @@ android {
minSdk = 21 minSdk = 21
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
} }
dependencies { dependencies {
@@ -2,15 +2,15 @@ package com.rtbishop.look4sat.data.framework
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.util.Log import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.OutputStream import java.io.OutputStream
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class BluetoothReporter( class BluetoothReporter(
private val manager: BluetoothManager, private val bluetoothManager: BluetoothManager,
private val reporterScope: CoroutineScope private val reporterScope: CoroutineScope
) { ) {
@@ -30,7 +30,7 @@ class BluetoothReporter(
if (!connected) { if (!connected) {
rotationConnectionJob = reporterScope.launch { rotationConnectionJob = reporterScope.launch {
try { try {
manager.adapter.getRemoteDevice(deviceId)?.let { device -> bluetoothManager.adapter.getRemoteDevice(deviceId)?.let { device ->
device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket -> device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket ->
connecting = true connecting = true
socket.connect() socket.connect()
@@ -22,36 +22,38 @@ import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.IDataSource import com.rtbishop.look4sat.domain.source.IFileSource
import com.rtbishop.look4sat.domain.source.ILocalStorage import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.source.IRemoteSource
import com.rtbishop.look4sat.domain.utility.DataParser import com.rtbishop.look4sat.domain.utility.DataParser
import java.io.InputStream
import java.util.zip.ZipInputStream
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.zip.ZipInputStream
class DatabaseRepo( class DatabaseRepo(
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
private val dataParser: DataParser, private val dataParser: DataParser,
private val dataSource: IDataSource, private val fileSource: IFileSource,
private val localStorage: ILocalStorage, private val localSource: ILocalSource,
private val settingsRepository: ISettingsRepo private val remoteSource: IRemoteSource,
private val settingsRepo: ISettingsRepo
) : IDatabaseRepo { ) : IDatabaseRepo {
override suspend fun updateFromFile(uri: String) = withContext(dispatcher) { override suspend fun updateFromFile(uri: String) = withContext(dispatcher) {
val importedSatellites = dataSource.getFileStream(uri)?.let { importSatellites(it) } val importedSatellites = fileSource.getFileStream(uri)?.let { importSatellites(it) }
importedSatellites?.let { localStorage.insertEntries(it) } importedSatellites?.let { localSource.insertEntries(it) }
setUpdateSuccessful(System.currentTimeMillis()) setUpdateSuccessful(System.currentTimeMillis())
} }
override suspend fun updateFromWeb() = withContext(dispatcher) { override suspend fun updateFromRemote() = withContext(dispatcher) {
val sourcesMap = settingsRepository.satelliteSourcesMap val sourcesMap = settingsRepo.satelliteSourcesMap
val importedEntries = mutableListOf<SatEntry>() val importedEntries = mutableListOf<SatEntry>()
val importedRadios = mutableListOf<SatRadio>() val importedRadios = mutableListOf<SatRadio>()
// fetch // fetch
val jobsMap = sourcesMap.mapValues { async { dataSource.getNetworkStream(it.value) } } val jobsMap = sourcesMap.mapValues { async { remoteSource.getRemoteStream(it.value) } }
val jobRadios = async { dataSource.getNetworkStream(settingsRepository.radioSourceUrl) } val jobRadios = async { remoteSource.getRemoteStream(settingsRepo.radioSourceUrl) }
// parse // parse
jobsMap.mapValues { job -> job.value.await() }.forEach { entry -> jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream -> entry.value?.let { stream ->
@@ -60,7 +62,7 @@ class DatabaseRepo(
// parse tle stream // parse tle stream
val satellites = importSatellites(stream) val satellites = importSatellites(stream)
val catnums = satellites.map { it.data.catnum } val catnums = satellites.map { it.data.catnum }
settingsRepository.saveSatType(type, catnums) settingsRepo.saveSatType(type, catnums)
importedEntries.addAll(satellites) importedEntries.addAll(satellites)
} }
@@ -69,7 +71,7 @@ class DatabaseRepo(
val unzipped = ZipInputStream(stream).apply { nextEntry } val unzipped = ZipInputStream(stream).apply { nextEntry }
val satellites = importSatellites(unzipped) val satellites = importSatellites(unzipped)
val catnums = satellites.map { it.data.catnum } val catnums = satellites.map { it.data.catnum }
settingsRepository.saveSatType(type, catnums) settingsRepo.saveSatType(type, catnums)
importedEntries.addAll(satellites) importedEntries.addAll(satellites)
} }
@@ -78,7 +80,7 @@ class DatabaseRepo(
val parsed = dataParser.parseCSVStream(stream) val parsed = dataParser.parseCSVStream(stream)
val satellites = parsed.map { data -> SatEntry(data) } val satellites = parsed.map { data -> SatEntry(data) }
val catnums = satellites.map { it.data.catnum } val catnums = satellites.map { it.data.catnum }
settingsRepository.saveSatType(type, catnums) settingsRepo.saveSatType(type, catnums)
importedEntries.addAll(satellites) importedEntries.addAll(satellites)
} }
} }
@@ -86,21 +88,21 @@ class DatabaseRepo(
} }
jobRadios.await()?.let { importedRadios.addAll(dataParser.parseJSONStream(it)) } jobRadios.await()?.let { importedRadios.addAll(dataParser.parseJSONStream(it)) }
// insert // insert
localStorage.insertEntries(importedEntries) localSource.insertEntries(importedEntries)
localStorage.insertRadios(importedRadios) localSource.insertRadios(importedRadios)
setUpdateSuccessful(System.currentTimeMillis()) setUpdateSuccessful(System.currentTimeMillis())
} }
override suspend fun clearAllData() = withContext(dispatcher) { override suspend fun clearAllData() = withContext(dispatcher) {
localStorage.deleteEntries() localSource.deleteEntries()
localStorage.deleteRadios() localSource.deleteRadios()
setUpdateSuccessful(0L) setUpdateSuccessful(0L)
} }
private suspend fun setUpdateSuccessful(timestamp: Long) = withContext(dispatcher) { private suspend fun setUpdateSuccessful(timestamp: Long) = withContext(dispatcher) {
val satellitesTotal = localStorage.getEntriesTotal() val satellitesTotal = localSource.getEntriesTotal()
val radiosTotal = localStorage.getRadiosTotal() val radiosTotal = localSource.getRadiosTotal()
settingsRepository.saveDatabaseState(DatabaseState(satellitesTotal, radiosTotal, timestamp)) settingsRepo.saveDatabaseState(DatabaseState(satellitesTotal, radiosTotal, timestamp))
} }
private suspend fun importSatellites(stream: InputStream): List<SatEntry> { private suspend fun importSatellites(stream: InputStream): List<SatEntry> {
@@ -24,7 +24,7 @@ import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.ILocalStorage import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.utility.round import com.rtbishop.look4sat.domain.utility.round
import com.rtbishop.look4sat.domain.utility.toDegrees import com.rtbishop.look4sat.domain.utility.toDegrees
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@@ -35,7 +35,7 @@ import kotlinx.coroutines.withContext
class SatelliteRepo( class SatelliteRepo(
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
private val localStorage: ILocalStorage, private val localStorage: ILocalSource,
private val settingsRepo: ISettingsRepo private val settingsRepo: ISettingsRepo
) : ISatelliteRepo { ) : ISatelliteRepo {
@@ -3,7 +3,7 @@ package com.rtbishop.look4sat.data.repository
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.repository.ISelectionRepo import com.rtbishop.look4sat.domain.repository.ISelectionRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.ILocalStorage import com.rtbishop.look4sat.domain.source.ILocalSource
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -15,7 +15,7 @@ import kotlinx.coroutines.withContext
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class SelectionRepo( class SelectionRepo(
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
private val localStorage: ILocalStorage, private val localSource: ILocalSource,
private val settingsRepo: ISettingsRepo private val settingsRepo: ISettingsRepo
) : ISelectionRepo { ) : ISelectionRepo {
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList()) private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
@@ -34,7 +34,7 @@ class SelectionRepo(
override suspend fun getEntriesFlow() = withContext(dispatcher) { override suspend fun getEntriesFlow() = withContext(dispatcher) {
val selectedIds = settingsRepo.satelliteSelection.value val selectedIds = settingsRepo.satelliteSelection.value
currentItems.value = localStorage.getEntriesList().map { item -> currentItems.value = localSource.getEntriesList().map { item ->
item.copy(isSelected = item.catnum in selectedIds) item.copy(isSelected = item.catnum in selectedIds)
} }
return@withContext itemsWithQuery return@withContext itemsWithQuery
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.rtbishop.look4sat.data.framework package com.rtbishop.look4sat.data.repository
import android.content.SharedPreferences import android.content.SharedPreferences
import android.location.Criteria import android.location.Criteria
@@ -28,14 +28,15 @@ import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.PassesSettings import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.repository.ISettingsRepo import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.QthConverter import com.rtbishop.look4sat.domain.utility.positionToQth
import com.rtbishop.look4sat.domain.utility.qthToPosition
import com.rtbishop.look4sat.domain.utility.round import com.rtbishop.look4sat.domain.utility.round
import java.util.concurrent.Executors
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.util.concurrent.Executors
class SettingsRepo( class SettingsRepo(
private val manager: LocationManager, private val preferences: SharedPreferences private val locationManager: LocationManager, private val preferences: SharedPreferences
) : ISettingsRepo { ) : ISettingsRepo {
private val keyModes = "satModes" private val keyModes = "satModes"
@@ -73,12 +74,12 @@ class SettingsRepo(
override val stationPosition: StateFlow<GeoPos> = _stationPosition override val stationPosition: StateFlow<GeoPos> = _stationPosition
override fun setGpsPosition(): Boolean { override fun setGpsPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(manager)) return false if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
try { try {
val criteria = Criteria().apply { isCostAllowed = true } val criteria = Criteria().apply { isCostAllowed = true }
val provider = manager.getBestProvider(criteria, true) ?: defaultProvider val provider = locationManager.getBestProvider(criteria, true) ?: defaultProvider
println("Requesting location for $provider provider") println("Requesting location for $provider provider")
LocationManagerCompat.getCurrentLocation(manager, provider, timeoutSignal, executor) { LocationManagerCompat.getCurrentLocation(locationManager, provider, timeoutSignal, executor) {
it?.let { setGeoPosition(it.latitude, it.longitude, it.altitude) } it?.let { setGeoPosition(it.latitude, it.longitude, it.altitude) }
} }
} catch (exception: SecurityException) { } catch (exception: SecurityException) {
@@ -89,13 +90,13 @@ class SettingsRepo(
override fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double): Boolean { override fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double): Boolean {
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
val locator = QthConverter.positionToQth(latitude, newLongitude) ?: return false val locator = positionToQth(latitude, newLongitude) ?: return false
saveGeoPos(latitude, newLongitude, altitude, locator) saveGeoPos(latitude, newLongitude, altitude, locator)
return true return true
} }
override fun setQthPosition(locator: String): Boolean { override fun setQthPosition(locator: String): Boolean {
val position = QthConverter.qthToPosition(locator) ?: return false val position = qthToPosition(locator) ?: return false
saveGeoPos(position.latitude, position.longitude, 0.0, locator) saveGeoPos(position.latitude, position.longitude, 0.0, locator)
return true return true
} }
@@ -0,0 +1,16 @@
package com.rtbishop.look4sat.data.source
import android.content.ContentResolver
import android.net.Uri
import com.rtbishop.look4sat.domain.source.IFileSource
import java.io.InputStream
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
class FileSource(
private val contentResolver: ContentResolver, private val dispatcher: CoroutineDispatcher
) : IFileSource {
override suspend fun getFileStream(uri: String): InputStream? {
return withContext(dispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
}
}
@@ -17,18 +17,18 @@
*/ */
package com.rtbishop.look4sat.data.source package com.rtbishop.look4sat.data.source
import com.rtbishop.look4sat.data.database.dao.StorageDao
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.source.ILocalStorage
import com.rtbishop.look4sat.data.database.entity.SatEntry as FrameworkEntry import com.rtbishop.look4sat.data.database.entity.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.data.database.entity.SatItem as FrameworkItem import com.rtbishop.look4sat.data.database.entity.SatItem as FrameworkItem
import com.rtbishop.look4sat.data.database.entity.SatRadio as FrameworkRadio import com.rtbishop.look4sat.data.database.entity.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio
import com.rtbishop.look4sat.data.database.dao.StorageDao
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.source.ILocalSource
class LocalStorage(private val storageDao: StorageDao) : ILocalStorage { class LocalSource(private val storageDao: StorageDao) : ILocalSource {
//region # Entries region //region # Entries region
@@ -17,26 +17,18 @@
*/ */
package com.rtbishop.look4sat.data.source package com.rtbishop.look4sat.data.source
import android.content.ContentResolver import com.rtbishop.look4sat.domain.source.IRemoteSource
import android.net.Uri import java.io.InputStream
import com.rtbishop.look4sat.domain.source.IDataSource
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.io.InputStream
class DataSource( class RemoteSource(
private val contentResolver: ContentResolver, private val httpClient: OkHttpClient, private val dispatcher: CoroutineDispatcher
private val httpClient: OkHttpClient, ) : IRemoteSource {
private val dispatcher: CoroutineDispatcher
) : IDataSource {
override suspend fun getFileStream(uri: String): InputStream? { override suspend fun getRemoteStream(url: String): InputStream? = withContext(dispatcher) {
return withContext(dispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
}
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
try { try {
val request = Request.Builder().url(url).build() val request = Request.Builder().url(url).build()
httpClient.newCall(request).execute().body?.byteStream() httpClient.newCall(request).execute().body?.byteStream()
@@ -19,8 +19,6 @@ package com.rtbishop.look4sat.domain.model
sealed class DataState<out T> { sealed class DataState<out T> {
data class Success<out T>(val data: T) : DataState<T>() data class Success<out T>(val data: T) : DataState<T>()
data class Error(val resId: Int?) : DataState<Nothing>()
data class Exception(val message: String?) : DataState<Nothing>() data class Exception(val message: String?) : DataState<Nothing>()
data object Handled : DataState<Nothing>()
data object Loading : DataState<Nothing>() data object Loading : DataState<Nothing>()
} }
@@ -21,7 +21,7 @@ interface IDatabaseRepo {
suspend fun updateFromFile(uri: String) suspend fun updateFromFile(uri: String)
suspend fun updateFromWeb() suspend fun updateFromRemote()
suspend fun clearAllData() suspend fun clearAllData()
} }
@@ -19,9 +19,6 @@ package com.rtbishop.look4sat.domain.source
import java.io.InputStream import java.io.InputStream
interface IDataSource { interface IFileSource {
suspend fun getFileStream(uri: String): InputStream? suspend fun getFileStream(uri: String): InputStream?
suspend fun getNetworkStream(url: String): InputStream?
} }
@@ -22,7 +22,7 @@ import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.domain.predict.Satellite
interface ILocalStorage { interface ILocalSource {
suspend fun getEntriesTotal(): Int suspend fun getEntriesTotal(): Int
@@ -0,0 +1,24 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.source
import java.io.InputStream
interface IRemoteSource {
suspend fun getRemoteStream(url: String): InputStream?
}
@@ -1,3 +1,20 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.utility package com.rtbishop.look4sat.domain.utility
import com.rtbishop.look4sat.domain.predict.DEG2RAD import com.rtbishop.look4sat.domain.predict.DEG2RAD
@@ -19,43 +19,40 @@ package com.rtbishop.look4sat.domain.utility
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
object QthConverter { fun qthToPosition(locator: String): GeoPos? {
val trimmedQth = locator.take(6)
fun qthToPosition(locator: String): GeoPos? { if (!isValidLocator(trimmedQth)) return null
val trimmedQth = locator.take(6) val lonFirst = (trimmedQth[0].uppercaseChar().code - 65) * 20
if (!isValidLocator(trimmedQth)) return null val latFirst = (trimmedQth[1].uppercaseChar().code - 65) * 10
val lonFirst = (trimmedQth[0].uppercaseChar().code - 65) * 20 val lonSecond = trimmedQth[2].toString().toInt() * 2
val latFirst = (trimmedQth[1].uppercaseChar().code - 65) * 10 val latSecond = trimmedQth[3].toString().toInt()
val lonSecond = trimmedQth[2].toString().toInt() * 2 val lonThird = (((trimmedQth[4].lowercaseChar().code - 97) / 12.0) + (1.0 / 24.0)) - 180
val latSecond = trimmedQth[3].toString().toInt() val latThird = (((trimmedQth[5].lowercaseChar().code - 97) / 24.0) + (1.0 / 48.0)) - 90
val lonThird = (((trimmedQth[4].lowercaseChar().code - 97) / 12.0) + (1.0 / 24.0)) - 180 val longitude = (lonFirst + lonSecond + lonThird).round(4)
val latThird = (((trimmedQth[5].lowercaseChar().code - 97) / 24.0) + (1.0 / 48.0)) - 90 val latitude = (latFirst + latSecond + latThird).round(4)
val longitude = (lonFirst + lonSecond + lonThird).round(4) return GeoPos(latitude, longitude)
val latitude = (latFirst + latSecond + latThird).round(4) }
return GeoPos(latitude, longitude)
} fun positionToQth(latitude: Double, longitude: Double): String? {
if (!isValidPosition(latitude, longitude)) return null
fun positionToQth(latitude: Double, longitude: Double): String? { val tempLon = if (longitude > 180.0) longitude - 180 else longitude
if (!isValidPosition(latitude, longitude)) return null val upper = "ABCDEFGHIJKLMNOPQRSTUVWX"
val tempLon = if (longitude > 180.0) longitude - 180 else longitude val lower = "abcdefghijklmnopqrstuvwx"
val upper = "ABCDEFGHIJKLMNOPQRSTUVWX" val newLongitude = tempLon + 180
val lower = "abcdefghijklmnopqrstuvwx" val newLatitude = latitude + 90
val newLongitude = tempLon + 180 val lonFirst = upper[(newLongitude / 20).toInt()]
val newLatitude = latitude + 90 val latFirst = upper[(newLatitude / 10).toInt()]
val lonFirst = upper[(newLongitude / 20).toInt()] val lonSecond = ((newLongitude / 2) % 10).toInt().toString()
val latFirst = upper[(newLatitude / 10).toInt()] val latSecond = (newLatitude % 10).toInt().toString()
val lonSecond = ((newLongitude / 2) % 10).toInt().toString() val lonThird = lower[((newLongitude % 2) * 12).toInt()]
val latSecond = (newLatitude % 10).toInt().toString() val latThird = lower[((newLatitude % 1) * 24).toInt()]
val lonThird = lower[((newLongitude % 2) * 12).toInt()] return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
val latThird = lower[((newLatitude % 1) * 24).toInt()] }
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
} private fun isValidPosition(lat: Double, lon: Double): Boolean {
return (lat >= -90.0 && lat <= 90.0) && (lon >= -180.0 && lon <= 360.0)
private fun isValidPosition(lat: Double, lon: Double): Boolean { }
return (lat >= -90.0 && lat <= 90.0) && (lon >= -180.0 && lon <= 360.0)
} private fun isValidLocator(locator: String): Boolean {
return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
private fun isValidLocator(locator: String): Boolean {
return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
}
} }
@@ -17,37 +17,38 @@
*/ */
package com.rtbishop.look4sat.domain package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.utility.QthConverter import com.rtbishop.look4sat.domain.utility.positionToQth
import com.rtbishop.look4sat.domain.utility.qthToPosition
import org.junit.Test import org.junit.Test
class QthConverterTest { class QthConverterTest {
@Test @Test
fun `Given valid QTH returns correct POS`() { fun `Given valid QTH returns correct POS`() {
var result = QthConverter.qthToPosition("io91VL39FX") var result = qthToPosition("io91VL39FX")
assert(result?.latitude == 51.4792 && result.longitude == -0.2083) assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
result = QthConverter.qthToPosition("JN58TD") result = qthToPosition("JN58TD")
assert(result?.latitude == 48.1458 && result.longitude == 11.6250) assert(result?.latitude == 48.1458 && result.longitude == 11.6250)
result = QthConverter.qthToPosition("gf15vc") result = qthToPosition("gf15vc")
assert(result?.latitude == -34.8958 && result.longitude == -56.2083) assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
} }
@Test @Test
fun `Given invalid QTH returns null`() { fun `Given invalid QTH returns null`() {
assert(QthConverter.qthToPosition("ZZ00zz") == null) assert(qthToPosition("ZZ00zz") == null)
assert(QthConverter.qthToPosition("JN58") == null) assert(qthToPosition("JN58") == null)
} }
@Test @Test
fun `Given valid POS returns correct QTH`() { fun `Given valid POS returns correct QTH`() {
assert(QthConverter.positionToQth(51.4878, -0.2146) == "IO91vl") assert(positionToQth(51.4878, -0.2146) == "IO91vl")
assert(QthConverter.positionToQth(48.1466, 11.6083) == "JN58td") assert(positionToQth(48.1466, 11.6083) == "JN58td")
assert(QthConverter.positionToQth(-34.91, -56.2116) == "GF15vc") assert(positionToQth(-34.91, -56.2116) == "GF15vc")
} }
@Test @Test
fun `Given invalid POS returns null`() { fun `Given invalid POS returns null`() {
assert(QthConverter.positionToQth(91.0542, -170.1142) == null) assert(positionToQth(91.0542, -170.1142) == null)
assert(QthConverter.positionToQth(89.0542, -240.1142) == null) assert(positionToQth(89.0542, -240.1142) == null)
} }
} }