Interfaces and classes refactoring and code cleanup

This commit is contained in:
Arty Bishop committed 2022-02-22 19:30:07 +00:00
1 parent 0c71c4db29
commit 74cbad5127
43 files changed
+403 -521

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".presentation.Look4SatApplication"
android:name=".presentation.MainApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
@@ -1,47 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 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 com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio
import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment)
fun DomainRadio.toFramework() = FrameworkRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, this.modes, false)
fun FrameworkRadio.toDomain() = DomainRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
fun List<DomainEntry>.toFrameworkEntries() = this.map { it.toFramework() }
fun List<DomainRadio>.toFrameworkRadios() = this.map { it.toFramework() }
fun List<FrameworkItem>.toDomainItems() = this.map { it.toDomain() }
fun List<FrameworkRadio>.toDomainRadios() = this.map { it.toDomain() }
@@ -26,8 +26,8 @@ import android.location.LocationManager
import android.os.Bundle
import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.ILocationHandler
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.domain.QthConverter
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
@@ -41,7 +41,7 @@ import javax.inject.Singleton
@Singleton
class LocationHandler @Inject constructor(
@ApplicationContext private val context: Context,
private val settingsHandler: ISettingsHandler,
private val settings: ISettings,
) : LocationListener, ILocationHandler {
private val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
@@ -51,8 +51,8 @@ class LocationHandler @Inject constructor(
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 = settingsHandler.loadStationLocator()
private var currentPosition = settingsHandler.loadStationPosition()
private var currentLocator = settings.loadStationLocator()
private var currentPosition = settings.loadStationPosition()
override val stationPosition: StateFlow<DataState<GeoPos>> = _stationPosition
@@ -68,8 +68,8 @@ class LocationHandler @Inject constructor(
QthConverter.positionToQth(newLat, newLon)?.let { locator ->
currentLocator = locator
currentPosition = GeoPos(newLat, newLon)
settingsHandler.saveStationLocator(locator)
settingsHandler.saveStationPosition(currentPosition)
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
}
} else {
@@ -113,8 +113,8 @@ class LocationHandler @Inject constructor(
if (position != null) {
currentLocator = locator
currentPosition = position
settingsHandler.saveStationLocator(locator)
settingsHandler.saveStationPosition(currentPosition)
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.pref_pos_qth_error))
@@ -19,15 +19,13 @@ package com.rtbishop.look4sat.framework
import android.content.SharedPreferences
import androidx.core.content.edit
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.presentation.getDouble
import com.rtbishop.look4sat.presentation.putDouble
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SettingsHandler @Inject constructor(private val prefs: SharedPreferences) : ISettingsHandler {
class SettingsHandler @Inject constructor(private val prefs: SharedPreferences) : ISettings {
companion object {
const val keyDataSources = "dataSources"
@@ -158,4 +156,12 @@ class SettingsHandler @Inject constructor(private val prefs: SharedPreferences)
override fun saveDataSources(sources: List<String>) {
if (sources.isNotEmpty()) prefs.edit { putStringSet(keyDataSources, sources.toSet()) }
}
private fun SharedPreferences.getDouble(key: String, default: Double): Double {
return Double.fromBits(getLong(key, default.toRawBits()))
}
private fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits())
}
}
@@ -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.framework.local
package com.rtbishop.look4sat.framework.data
import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatEntry
@@ -23,18 +23,18 @@ import com.rtbishop.look4sat.framework.model.SatItem
import kotlinx.coroutines.flow.Flow
@Dao
interface SatEntriesDao {
interface EntriesDao {
@Query("SELECT COUNT(*) FROM entries")
fun getEntriesNumber(): Flow<Int>
fun getEntriesTotal(): Flow<Int>
@Transaction
@Query("SELECT catnum, name FROM entries ORDER BY name ASC")
suspend fun getEntriesWithModes(): List<SatItem>
@Transaction
@Query("SELECT * FROM entries WHERE catnum IN (:catnums)")
suspend fun getSelectedEntries(catnums: List<Int>): List<SatEntry>
@Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)")
suspend fun getEntriesWithIds(selectedIds: List<Int>): List<SatEntry>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEntries(entries: List<SatEntry>)
@@ -0,0 +1,25 @@
package com.rtbishop.look4sat.framework.data
import android.content.ContentResolver
import android.net.Uri
import com.rtbishop.look4sat.data.IProvider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.net.URL
class Provider(
private val contentResolver: ContentResolver,
private val ioDispatcher: CoroutineDispatcher
) : IProvider {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getLocalFileStream(uri: String): InputStream? {
return withContext(ioDispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
}
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getRemoteFileStream(url: String): InputStream? {
return withContext(ioDispatcher) { URL(url).openStream() }
}
}
@@ -15,23 +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.local
package com.rtbishop.look4sat.framework.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatRadio
import kotlinx.coroutines.flow.Flow
@Dao
interface SatRadiosDao {
interface RadiosDao {
@Query("SELECT COUNT(*) FROM radios")
fun getRadiosNumber(): Flow<Int>
fun getRadiosTotal(): Flow<Int>
@Query("SELECT * FROM radios WHERE catnum = :catnum AND isAlive = 1")
suspend fun getRadios(catnum: Int): List<SatRadio>
@Transaction
@Query("SELECT * FROM radios WHERE catnum = :id AND isAlive = 1")
suspend fun getRadiosWithId(id: Int): List<SatRadio>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRadios(radios: List<SatRadio>)
@@ -0,0 +1,86 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import com.rtbishop.look4sat.data.IStorage
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio
import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
class Storage(private val entriesDao: EntriesDao, private val radiosDao: RadiosDao) : IStorage {
override fun getEntriesTotal() = entriesDao.getEntriesTotal()
override fun getRadiosTotal() = radiosDao.getRadiosTotal()
override suspend fun getEntriesWithModes(): List<DomainItem> {
return entriesDao.getEntriesWithModes().toDomainItems()
}
override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> {
val selectedSatellites = mutableListOf<Satellite>()
ids.chunked(999).forEach { idsPart ->
val entries = entriesDao.getEntriesWithIds(idsPart)
selectedSatellites.addAll(entries.map { entry -> entry.data.createSat() })
}
return selectedSatellites
}
override suspend fun getRadiosWithId(id: Int): List<DomainRadio> {
return radiosDao.getRadiosWithId(id).toDomainRadios()
}
override suspend fun insertEntries(entries: List<DomainEntry>) {
entriesDao.insertEntries(entries.toFrameworkEntries())
}
override suspend fun insertRadios(radios: List<DomainRadio>) {
radiosDao.insertRadios(radios.toFrameworkRadios())
}
override suspend fun clearAllData() {
entriesDao.deleteEntries()
radiosDao.deleteRadios()
}
private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment)
private fun DomainRadio.toFramework() = FrameworkRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, this.modes, false)
private fun FrameworkRadio.toDomain() = DomainRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() }
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
private fun List<FrameworkItem>.toDomainItems() = this.map { item -> item.toDomain() }
private fun List<FrameworkRadio>.toDomainRadios() = this.map { radio -> radio.toDomain() }
}
@@ -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.framework.local
package com.rtbishop.look4sat.framework.data
import androidx.room.Database
import androidx.room.RoomDatabase
@@ -23,9 +23,9 @@ import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatRadio
@Database(entities = [SatEntry::class, SatRadio::class], version = 1, exportSchema = true)
abstract class Look4SatDb : RoomDatabase() {
abstract class StorageDb : RoomDatabase() {
abstract fun entriesDao(): SatEntriesDao
abstract fun entriesDao(): EntriesDao
abstract fun radiosDao(): SatRadiosDao
abstract fun radiosDao(): RadiosDao
}
@@ -1,101 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 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.local
import android.content.ContentResolver
import android.net.Uri
import com.rtbishop.look4sat.data.ILocalSource
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.framework.toDomainItems
import com.rtbishop.look4sat.framework.toDomainRadios
import com.rtbishop.look4sat.framework.toFrameworkEntries
import com.rtbishop.look4sat.framework.toFrameworkRadios
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.io.InputStream
class LocalSource(
private val entriesDao: SatEntriesDao,
private val radiosDao: SatRadiosDao,
private val settings: ISettingsHandler,
private val resolver: ContentResolver,
private val ioDispatcher: CoroutineDispatcher
) : ILocalSource {
override fun getEntriesNumber() = entriesDao.getEntriesNumber()
override fun getRadiosNumber() = radiosDao.getRadiosNumber()
override suspend fun getEntriesWithModes(): List<SatItem> {
val selectedCatnums = getEntriesSelection()
val entriesWithModes = entriesDao.getEntriesWithModes().toDomainItems()
entriesWithModes.forEach { entry -> entry.isSelected = entry.catnum in selectedCatnums }
return entriesWithModes
}
override suspend fun getSelectedEntries(): List<Satellite> {
val selectedSatellites = mutableListOf<Satellite>()
getEntriesSelection().chunked(999).forEach { catnums ->
val entries = entriesDao.getSelectedEntries(catnums)
selectedSatellites.addAll(entries.map { entry -> entry.data.createSat() })
}
return selectedSatellites
}
override suspend fun getRadios(catnum: Int): List<SatRadio> {
return radiosDao.getRadios(catnum).toDomainRadios()
}
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getFileStream(uri: String): InputStream? {
return withContext(ioDispatcher) { resolver.openInputStream(Uri.parse(uri)) }
}
override suspend fun insertEntries(entries: List<SatEntry>) {
entriesDao.insertEntries(entries.toFrameworkEntries())
}
override suspend fun insertRadios(radios: List<SatRadio>) {
radiosDao.insertRadios(radios.toFrameworkRadios())
}
override suspend fun clearAllData() {
entriesDao.deleteEntries()
radiosDao.deleteRadios()
}
override suspend fun getDataSources(): List<String> {
return withContext(ioDispatcher) { settings.loadDataSources() }
}
override suspend fun setDataSources(sources: List<String>) {
withContext(ioDispatcher) { settings.saveDataSources(sources) }
}
override suspend fun getEntriesSelection(): List<Int> {
return withContext(ioDispatcher) { settings.loadEntriesSelection() }
}
override suspend fun setEntriesSelection(catnums: List<Int>) {
withContext(ioDispatcher) { settings.saveEntriesSelection(catnums) }
}
}
@@ -1,15 +0,0 @@
package com.rtbishop.look4sat.framework.remote
import com.rtbishop.look4sat.data.IRemoteSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.net.URL
class RemoteSource(private val ioDispatcher: CoroutineDispatcher) : IRemoteSource {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getFileStream(url: String): InputStream? {
return withContext(ioDispatcher) { URL(url).openStream() }
}
}
@@ -20,18 +20,18 @@ package com.rtbishop.look4sat.injection
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import com.rtbishop.look4sat.data.DataRepository
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.DataParser
import com.rtbishop.look4sat.data.DataParser
import com.rtbishop.look4sat.data.Repository
import com.rtbishop.look4sat.domain.DataReporter
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationHandler
import com.rtbishop.look4sat.domain.IRepository
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.domain.predict.Predictor
import com.rtbishop.look4sat.framework.LocationHandler
import com.rtbishop.look4sat.framework.SettingsHandler
import com.rtbishop.look4sat.framework.local.LocalSource
import com.rtbishop.look4sat.framework.local.Look4SatDb
import com.rtbishop.look4sat.framework.remote.RemoteSource
import com.rtbishop.look4sat.framework.data.Provider
import com.rtbishop.look4sat.framework.data.Storage
import com.rtbishop.look4sat.framework.data.StorageDb
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -49,24 +49,22 @@ object CoreModule {
@Provides
@Singleton
fun provideSatelliteRepo(
settings: ISettingsHandler,
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
): IDataRepository {
val db = Room.databaseBuilder(context, Look4SatDb::class.java, "Look4SatDb")
): IRepository {
val db = Room.databaseBuilder(context, StorageDb::class.java, "Look4SatDb")
.fallbackToDestructiveMigration().build()
val dataParser = DataParser(defaultDispatcher)
val resolver = context.contentResolver
val local = LocalSource(db.entriesDao(), db.radiosDao(), settings, resolver, ioDispatcher)
val remote = RemoteSource(ioDispatcher)
val localSource = Storage(db.entriesDao(), db.radiosDao())
val remoteSource = Provider(context.contentResolver, ioDispatcher)
val repositoryScope = CoroutineScope(SupervisorJob())
return DataRepository(dataParser, local, remote, repositoryScope)
return Repository(dataParser, localSource, remoteSource, repositoryScope)
}
@Provides
@Singleton
fun provideSettingsHandler(sharedPreferences: SharedPreferences): ISettingsHandler {
fun provideSettingsHandler(sharedPreferences: SharedPreferences): ISettings {
return SettingsHandler(sharedPreferences)
}
@@ -74,9 +72,9 @@ object CoreModule {
@Singleton
fun provideLocationHandler(
@ApplicationContext context: Context,
settingsHandler: ISettingsHandler
settings: ISettings
): ILocationHandler {
return LocationHandler(context, settingsHandler)
return LocationHandler(context, settings)
}
@Provides
@@ -1,57 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 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.content.Context
import android.content.SharedPreferences
import android.widget.Toast
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.fragment.findNavController
fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, text, duration).apply { show() }
}
fun SharedPreferences.getDouble(key: String, default: Double): Double {
return Double.fromBits(getLong(key, default.toRawBits()))
}
fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits())
}
fun <T> Fragment.getNavResult(@IdRes id: Int, key: String, onResult: (result: T) -> Unit) {
val backStackEntry = findNavController().getBackStackEntry(id)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME && backStackEntry.savedStateHandle.contains(key)) {
backStackEntry.savedStateHandle.get<T>(key)?.let(onResult)
backStackEntry.savedStateHandle.remove<T>(key)
}
}
backStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) backStackEntry.lifecycle.removeObserver(observer)
})
}
fun <T> Fragment.setNavResult(key: String, value: T) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, value)
}
@@ -20,23 +20,42 @@ package com.rtbishop.look4sat.presentation
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.activity.viewModels
import androidx.annotation.IdRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.fragment.findNavController
import com.rtbishop.look4sat.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater).root)
mainViewModel.calculatePasses()
}
}
fun <T> Fragment.getNavResult(@IdRes id: Int, key: String, onResult: (result: T) -> Unit) {
val backStackEntry = findNavController().getBackStackEntry(id)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME && backStackEntry.savedStateHandle.contains(key)) {
backStackEntry.savedStateHandle.get<T>(key)?.let(onResult)
backStackEntry.savedStateHandle.remove<T>(key)
}
}
backStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) backStackEntry.lifecycle.removeObserver(observer)
})
}
fun <T> Fragment.setNavResult(key: String, value: T) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, value)
}
@@ -21,4 +21,4 @@ import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class Look4SatApplication : Application()
class MainApplication : Application()
@@ -1,30 +0,0 @@
package com.rtbishop.look4sat.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.predict.Predictor
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val predictor: Predictor,
private val preferences: ISettingsHandler,
private val dataRepository: IDataRepository
) : ViewModel() {
fun calculatePasses(
hoursAhead: Int = preferences.getHoursAhead(),
minElevation: Double = preferences.getMinElevation(),
timeRef: Long = System.currentTimeMillis()
) {
viewModelScope.launch {
val satellites = dataRepository.getSelectedEntries()
val stationPos = preferences.loadStationPosition()
predictor.forceCalculation(satellites, stationPos, timeRef, hoursAhead, minElevation)
}
}
}
@@ -39,8 +39,8 @@ import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class EntriesFragment : Fragment(R.layout.fragment_entries) {
private lateinit var binding: FragmentEntriesBinding
private val viewModel: EntriesViewModel by viewModels()
private lateinit var binding: FragmentEntriesBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -18,8 +18,8 @@
package com.rtbishop.look4sat.presentation.entriesScreen
import androidx.lifecycle.*
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.IRepository
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -28,13 +28,13 @@ import javax.inject.Inject
@HiltViewModel
class EntriesViewModel @Inject constructor(
private val preferences: ISettingsHandler,
private val repository: IDataRepository
private val repository: IRepository,
private val settings: ISettings
) : ViewModel(), EntriesAdapter.EntriesClickListener {
private val transModes = MutableLiveData(preferences.loadModesSelection())
private val transModes = MutableLiveData(settings.loadModesSelection())
private val currentQuery = MutableLiveData(String())
private val itemsFromRepo = liveData { emit(repository.getEntriesWithModes()) } as MutableLiveData
private val itemsFromRepo = liveData { emit(loadEntriesWithSelection()) } as MutableLiveData
private val itemsWithModes = transModes.switchMap { modes ->
itemsFromRepo.map { items -> filterByModes(items, modes) }
}
@@ -51,7 +51,7 @@ class EntriesViewModel @Inject constructor(
fun saveSelectedModes(modes: List<String>) {
transModes.value = modes
preferences.saveModesSelection(modes)
settings.saveModesSelection(modes)
}
fun setQuery(query: String) {
@@ -74,6 +74,11 @@ class EntriesViewModel @Inject constructor(
}
}
private suspend fun loadEntriesWithSelection(): List<SatItem> {
val selectedIds = settings.loadEntriesSelection()
return repository.getEntriesWithModes().onEach { it.isSelected = it.catnum in selectedIds }
}
private fun filterByModes(items: List<SatItem>, modes: List<String>): List<SatItem> {
if (modes.isEmpty()) return items
return items.filter { item -> item.modes.any { mode -> mode in modes } }
@@ -9,8 +9,8 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.databinding.DialogModesBinding
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -19,7 +19,7 @@ import javax.inject.Inject
class ModesDialog : AppCompatDialogFragment(), ModesAdapter.ModesClickListener {
@Inject
lateinit var preferences: ISettingsHandler
lateinit var preferences: ISettings
private lateinit var binding: DialogModesBinding
private val allModes = listOf(
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
@@ -17,7 +17,7 @@
*/
package com.rtbishop.look4sat.presentation.mapScreen
import android.content.SharedPreferences
import android.content.Context
import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
@@ -43,17 +43,12 @@ import org.osmdroid.views.overlay.FolderOverlay
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polygon
import org.osmdroid.views.overlay.Polyline
import javax.inject.Inject
@AndroidEntryPoint
class MapFragment : Fragment(R.layout.fragment_map) {
@Inject
lateinit var sharedPreferences: SharedPreferences
private lateinit var binding: FragmentMapBinding
private val args: MapFragmentArgs by navArgs()
private val viewModel: MapViewModel by viewModels()
private val navArgs: MapFragmentArgs by navArgs()
private val minLat = MapView.getTileSystem().minLatitude
private val maxLat = MapView.getTileSystem().maxLatitude
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
@@ -67,6 +62,7 @@ class MapFragment : Fragment(R.layout.fragment_map) {
style = Paint.Style.FILL_AND_STROKE
color = Color.parseColor("#26FFE082")
}
private lateinit var binding: FragmentMapBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -76,7 +72,9 @@ class MapFragment : Fragment(R.layout.fragment_map) {
}
private fun setupComponents() {
Configuration.getInstance().load(requireContext(), sharedPreferences)
val context = requireContext()
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
Configuration.getInstance().load(context, preferences)
binding.run {
mapView.apply {
setMultiTouchControls(true)
@@ -99,15 +97,15 @@ class MapFragment : Fragment(R.layout.fragment_map) {
}
private fun setupObservers() {
viewModel.selectDefaultSatellite(args.catNum)
viewModel.stationPosLiveData.observe(viewLifecycleOwner) { renderStationPos(it) }
viewModel.satPositions.observe(viewLifecycleOwner) { renderSatPositions(it) }
viewModel.satTrack.observe(viewLifecycleOwner) { renderSatTrack(it) }
viewModel.satFootprint.observe(viewLifecycleOwner) { renderSatFootprint(it) }
viewModel.mapData.observe(viewLifecycleOwner) { renderSatData(it) }
viewModel.selectDefaultSatellite(navArgs.catNum)
viewModel.stationPos.observe(viewLifecycleOwner) { handleStationPos(it) }
viewModel.positions.observe(viewLifecycleOwner) { handlePositions(it) }
viewModel.track.observe(viewLifecycleOwner) { handleTrack(it) }
viewModel.footprint.observe(viewLifecycleOwner) { handleFootprint(it) }
viewModel.mapData.observe(viewLifecycleOwner) { handleMapData(it) }
}
private fun renderStationPos(stationPos: GeoPos) {
private fun handleStationPos(stationPos: GeoPos) {
binding.apply {
Marker(mapView).apply {
setInfoWindow(null)
@@ -120,7 +118,7 @@ class MapFragment : Fragment(R.layout.fragment_map) {
}
}
private fun renderSatPositions(posMap: Map<Satellite, GeoPos>) {
private fun handlePositions(posMap: Map<Satellite, GeoPos>) {
binding.apply {
val markers = FolderOverlay()
posMap.entries.forEach {
@@ -149,7 +147,7 @@ class MapFragment : Fragment(R.layout.fragment_map) {
}
}
private fun renderSatTrack(satTrack: List<List<GeoPos>>) {
private fun handleTrack(satTrack: List<List<GeoPos>>) {
val trackOverlay = FolderOverlay()
satTrack.forEach { track ->
val trackPoints = track.map { GeoPoint(it.latitude, it.longitude) }
@@ -166,7 +164,7 @@ class MapFragment : Fragment(R.layout.fragment_map) {
binding.mapView.overlays[1] = trackOverlay
}
private fun renderSatFootprint(satFootprint: List<GeoPos>) {
private fun handleFootprint(satFootprint: List<GeoPos>) {
val footprintPoints = satFootprint.map { GeoPoint(it.latitude, it.longitude) }
val footprintOverlay = Polygon().apply {
fillPaint.set(footprintPaint)
@@ -180,7 +178,7 @@ class MapFragment : Fragment(R.layout.fragment_map) {
binding.mapView.overlays[2] = footprintOverlay
}
private fun renderSatData(mapData: MapData) {
private fun handleMapData(mapData: MapData) {
binding.apply {
mapDataName.text =
String.format(getString(R.string.pat_osm_idName), mapData.catNum, mapData.name)
@@ -18,8 +18,8 @@
package com.rtbishop.look4sat.presentation.mapScreen
import androidx.lifecycle.*
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.IRepository
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.domain.QthConverter
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.Predictor
@@ -33,52 +33,54 @@ import kotlin.math.min
@HiltViewModel
class MapViewModel @Inject constructor(
private val dataRepository: IDataRepository,
private val predictor: Predictor,
preferences: ISettingsHandler,
private val repository: IRepository,
private val settings: ISettings,
) : ViewModel() {
private val stationPos = preferences.loadStationPosition()
private val stationPosition = settings.loadStationPosition()
private var allSatellites = listOf<Satellite>()
private var dataUpdateJob: Job? = null
private var allSatList = listOf<Satellite>()
private lateinit var selectedSat: Satellite
private var dataUpdateRate = 2000L
private lateinit var selectedSatellite: Satellite
val stationPosLiveData = liveData {
val osmLat = clipLat(stationPos.latitude)
val osmLon = clipLon(stationPos.longitude)
val stationPos = liveData {
val osmLat = clipLat(stationPosition.latitude)
val osmLon = clipLon(stationPosition.longitude)
emit(GeoPos(osmLat, osmLon))
}
private val _satTrack = MutableLiveData<List<List<GeoPos>>>()
val satTrack: LiveData<List<List<GeoPos>>> = _satTrack
private val _track = MutableLiveData<List<List<GeoPos>>>()
val track: LiveData<List<List<GeoPos>>> = _track
private val _satFootprint = MutableLiveData<List<GeoPos>>()
val satFootprint: LiveData<List<GeoPos>> = _satFootprint
private val _footprint = MutableLiveData<List<GeoPos>>()
val footprint: LiveData<List<GeoPos>> = _footprint
private val _satData = MutableLiveData<MapData>()
val mapData: LiveData<MapData> = this._satData
private val _mapData = MutableLiveData<MapData>()
val mapData: LiveData<MapData> = this._mapData
private val _satPositions = MutableLiveData<Map<Satellite, GeoPos>>()
val satPositions: LiveData<Map<Satellite, GeoPos>> = _satPositions
private val _positions = MutableLiveData<Map<Satellite, GeoPos>>()
val positions: LiveData<Map<Satellite, GeoPos>> = _positions
fun scrollSelection(decrement: Boolean) {
if (allSatList.isNotEmpty()) {
val index = allSatList.indexOf(selectedSat)
if (allSatellites.isNotEmpty()) {
val index = allSatellites.indexOf(selectedSatellite)
if (decrement) {
if (index > 0) selectSatellite(allSatList[index - 1])
else selectSatellite(allSatList[allSatList.size - 1])
if (index > 0) selectSatellite(allSatellites[index - 1])
else selectSatellite(allSatellites[allSatellites.size - 1])
} else {
if (index < allSatList.size - 1) selectSatellite(allSatList[index + 1])
else selectSatellite(allSatList[0])
if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1])
else selectSatellite(allSatellites[0])
}
}
}
fun selectDefaultSatellite(catnum: Int) {
viewModelScope.launch {
dataRepository.getSelectedEntries().also { satellites ->
val selectedIds = settings.loadEntriesSelection()
repository.getEntriesWithIds(selectedIds).also { satellites ->
if (satellites.isNotEmpty()) {
allSatList = satellites
allSatellites = satellites
if (catnum == -1) {
selectSatellite(satellites.first())
} else {
@@ -89,18 +91,18 @@ class MapViewModel @Inject constructor(
}
}
fun selectSatellite(satellite: Satellite, updateFreq: Long = 2000) {
selectedSat = satellite
fun selectSatellite(satellite: Satellite, updateFreq: Long = dataUpdateRate) {
selectedSatellite = satellite
viewModelScope.launch {
dataUpdateJob?.cancelAndJoin()
dataUpdateJob = launch {
val dateNow = Date()
getSatTrack(selectedSat, stationPos, dateNow)
getSatTrack(selectedSatellite, stationPosition, dateNow)
while (isActive) {
dateNow.time = System.currentTimeMillis()
getPositions(allSatList, stationPos, dateNow)
getSatFootprint(selectedSat, stationPos, dateNow)
getSatData(selectedSat, stationPos, dateNow)
getPositions(allSatellites, stationPosition, dateNow)
getSatFootprint(selectedSatellite, stationPosition, dateNow)
getSatData(selectedSatellite, stationPosition, dateNow)
delay(updateFreq)
}
}
@@ -133,7 +135,7 @@ class MapViewModel @Inject constructor(
currentTrack.add(currentPosition)
}
satTracks.add(currentTrack)
_satTrack.postValue(satTracks)
_track.postValue(satTracks)
}
private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) {
@@ -144,7 +146,7 @@ class MapViewModel @Inject constructor(
val osmLon = clipLon(Math.toDegrees(satPos.longitude))
positions[satellite] = GeoPos(osmLat, osmLon)
}
_satPositions.postValue(positions)
_positions.postValue(positions)
}
private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) {
@@ -154,7 +156,7 @@ class MapViewModel @Inject constructor(
val osmLon = clipLon(rangePos.longitude)
GeoPos(osmLat, osmLon)
}
_satFootprint.postValue(satFootprint)
_footprint.postValue(satFootprint)
}
private suspend fun getSatData(satellite: Satellite, pos: GeoPos, date: Date) {
@@ -167,7 +169,7 @@ class MapViewModel @Inject constructor(
satellite, satellite.data.catnum, satellite.data.name, satPos.distance,
satPos.altitude, satPos.getOrbitalVelocity(), qthLoc, osmPos
)
_satData.postValue(satData)
_mapData.postValue(satData)
}
private fun clipLat(latitude: Double): Double {
@@ -7,8 +7,8 @@ import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.databinding.DialogFilterBinding
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -17,7 +17,7 @@ import javax.inject.Inject
class FilterDialog : AppCompatDialogFragment() {
@Inject
lateinit var preferences: ISettingsHandler
lateinit var preferences: ISettings
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_filter, group, false)
@@ -18,9 +18,11 @@
package com.rtbishop.look4sat.presentation.passesScreen
import android.animation.ValueAnimator
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.animation.LinearInterpolator
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
@@ -34,7 +36,6 @@ import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.toTimerString
import com.rtbishop.look4sat.presentation.getNavResult
import com.rtbishop.look4sat.presentation.toast
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
@@ -83,8 +84,8 @@ class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesC
viewModel.passes.observe(viewLifecycleOwner) { passesResult ->
handleNewPasses(passesResult, adapter)
}
viewModel.entries.observe(viewLifecycleOwner) { number ->
handleEntriesNumber(number)
viewModel.entriesTotal.observe(viewLifecycleOwner) { number ->
handleEntriesTotal(number)
}
getNavResult<Pair<Int, Double>>(R.id.nav_passes, "prefs") { prefs ->
viewModel.calculatePasses(prefs.first, prefs.second)
@@ -103,7 +104,7 @@ class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesC
}
}
private fun handleEntriesNumber(number: Int) {
private fun handleEntriesTotal(number: Int) {
if (number > 0) {
binding.passesFab.setOnClickListener { navigateToEntries() }
} else {
@@ -190,4 +191,8 @@ class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesC
findNavController().navigate(action)
}
}
private fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, text, duration).apply { show() }
}
}
@@ -18,8 +18,8 @@
package com.rtbishop.look4sat.presentation.passesScreen
import androidx.lifecycle.*
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.IRepository
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.Predictor
import com.rtbishop.look4sat.domain.predict.SatPass
@@ -30,14 +30,14 @@ import javax.inject.Inject
@HiltViewModel
class PassesViewModel @Inject constructor(
private val predictor: Predictor,
private val settings: ISettingsHandler,
private val repository: IDataRepository
private val repository: IRepository,
private val settings: ISettings
) : ViewModel() {
private var passesProcessing: Job? = null
private val _passes = MutableLiveData<DataState<List<SatPass>>>(DataState.Loading)
val passes: LiveData<DataState<List<SatPass>>> = _passes
val entries: LiveData<Int> = repository.getEntriesNumber().asLiveData()
val entriesTotal: LiveData<Int> = repository.getEntriesTotal().asLiveData()
init {
viewModelScope.launch {
@@ -53,6 +53,7 @@ class PassesViewModel @Inject constructor(
}
}
}
calculatePasses()
}
fun shouldUseUTC() = settings.getUseUTC()
@@ -66,11 +67,12 @@ class PassesViewModel @Inject constructor(
viewModelScope.launch {
_passes.postValue(DataState.Loading)
passesProcessing?.cancelAndJoin()
selection?.let { items -> repository.setEntriesSelection(items) }
selection?.let { items -> settings.saveEntriesSelection(items) }
settings.setHoursAhead(hoursAhead)
settings.setMinElevation(minElevation)
val satellites = repository.getSelectedEntries()
val stationPos = settings.loadStationPosition()
val selectedIds = settings.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds)
predictor.forceCalculation(satellites, stationPos, timeRef, hoursAhead, minElevation)
}
}
@@ -27,31 +27,27 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.databinding.FragmentRadarBinding
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.toTimerString
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class RadarFragment : Fragment(R.layout.fragment_radar) {
@Inject
lateinit var preferences: ISettingsHandler
private lateinit var binding: FragmentRadarBinding
private val args: RadarFragmentArgs by navArgs()
private val viewModel: RadarViewModel by viewModels()
private var radarView: RadarView? = null
private val navArgs: RadarFragmentArgs by navArgs()
private lateinit var binding: FragmentRadarBinding
private lateinit var radarView: RadarView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentRadarBinding.bind(view)
setupViews()
setupComponents()
}
private fun setupViews() {
private fun setupComponents() {
val context = requireContext()
val adapter = RadioAdapter()
val layoutManager = LinearLayoutManager(context)
@@ -69,15 +65,15 @@ class RadarFragment : Fragment(R.layout.fragment_radar) {
}
private fun setupObservers(radioAdapter: RadioAdapter) {
viewModel.getPass(args.catNum, args.aosTime).observe(viewLifecycleOwner) { pass ->
viewModel.getPass(navArgs.catNum, navArgs.aosTime).observe(viewLifecycleOwner) { pass ->
radarView = RadarView(requireContext()).apply {
setShowAim(preferences.getUseCompass())
setScanning(preferences.getShowSweep())
setShowAim(viewModel.getUseCompass())
setScanning(viewModel.getShowSweep())
}
binding.radarCard.addView(radarView)
viewModel.radarData.observe(viewLifecycleOwner) { passData ->
radarView?.setPosition(passData.satPos)
radarView?.setPositions(passData.satTrack)
radarView.setPosition(passData.satPos)
radarView.setPositions(passData.satTrack)
setPassText(pass, passData.satPos)
}
viewModel.transmitters.observe(viewLifecycleOwner) { list ->
@@ -87,16 +83,15 @@ class RadarFragment : Fragment(R.layout.fragment_radar) {
} else {
binding.radarRecyclerMsg.text = getString(R.string.trans_no_data)
}
radarView?.invalidate()
radarView.invalidate()
}
viewModel.orientation.observe(viewLifecycleOwner) { orientation ->
radarView?.setOrientation(
orientation.first,
orientation.second,
orientation.third
)
viewModel.orientation.observe(viewLifecycleOwner) { value ->
radarView.setOrientation(value.first, value.second, value.third)
}
binding.radarBack.setOnClickListener { findNavController().navigateUp() }
binding.radarMap.setOnClickListener { navigateToMap(pass.catNum) }
binding.radarBtnNotify.isEnabled = false
binding.radarBtnSettings.setOnClickListener { navigateToSettings() }
}
}
@@ -106,11 +101,12 @@ class RadarFragment : Fragment(R.layout.fragment_radar) {
val radarElev = getString(R.string.radar_el_value)
val radarAlt = getString(R.string.radar_alt_value)
val radarDist = getString(R.string.radar_dist_value)
val radarId = getString(R.string.radar_sat_id)
binding.radarAzValue.text = String.format(radarAzim, Math.toDegrees(satPos.azimuth))
binding.radarElValue.text = String.format(radarElev, Math.toDegrees(satPos.elevation))
binding.radarAltValue.text = String.format(radarAlt, satPos.altitude)
binding.radarDstValue.text = String.format(radarDist, satPos.distance)
// binding.radarName.text = satPass.name
binding.radarSatId.text = String.format(radarId, satPass.catNum)
if (!satPass.isDeepSpace) {
if (timeNow < satPass.aosTime) {
@@ -134,6 +130,11 @@ class RadarFragment : Fragment(R.layout.fragment_radar) {
findNavController().navigate(direction)
}
private fun navigateToSettings() {
val direction = RadarFragmentDirections.actionGlobalSettingsFragment()
findNavController().navigate(direction)
}
override fun onResume() {
super.onResume()
viewModel.enableSensor()
@@ -19,9 +19,9 @@ package com.rtbishop.look4sat.presentation.radarScreen
import android.hardware.GeomagneticField
import androidx.lifecycle.*
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.DataReporter
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.IRepository
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.Predictor
@@ -39,13 +39,13 @@ import javax.inject.Inject
@HiltViewModel
class RadarViewModel @Inject constructor(
private val orientationHandler: OrientationHandler,
private val preferences: ISettingsHandler,
private val reporter: DataReporter,
private val predictor: Predictor,
private val dataRepository: IDataRepository,
private val dataReporter: DataReporter
private val repository: IRepository,
private val settings: ISettings
) : ViewModel(), OrientationHandler.OrientationListener {
private val stationPos = preferences.loadStationPosition()
private val stationPos = settings.loadStationPosition()
private val _passData = MutableLiveData<RadarData>()
private val _transmitters = MutableLiveData<List<SatRadio>>()
private val _orientation = MutableLiveData<Triple<Float, Float, Float>>()
@@ -55,7 +55,7 @@ class RadarViewModel @Inject constructor(
fun getPass(catNum: Int, aosTime: Long) = liveData {
predictor.calculatedPasses.collect { passes ->
val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime }
val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime }
pass?.let { satPass ->
emit(satPass)
sendPassData(satPass)
@@ -65,13 +65,17 @@ class RadarViewModel @Inject constructor(
}
fun enableSensor() {
if (preferences.getUseCompass()) orientationHandler.startListening(this)
if (settings.getUseCompass()) orientationHandler.startListening(this)
}
fun disableSensor() {
if (preferences.getUseCompass()) orientationHandler.stopListening()
if (settings.getUseCompass()) orientationHandler.stopListening()
}
fun getUseCompass(): Boolean = settings.getUseCompass()
fun getShowSweep(): Boolean = settings.getShowSweep()
override fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float) {
_orientation.value = Triple(azimuth + getMagDeclination(stationPos), pitch, roll)
}
@@ -92,12 +96,12 @@ class RadarViewModel @Inject constructor(
}
while (isActive) {
val satPos = predictor.getSatPos(satPass.satellite, stationPos, Date().time)
if (preferences.getRotatorEnabled()) {
val server = preferences.getRotatorServer()
val port = preferences.getRotatorPort().toInt()
if (settings.getRotatorEnabled()) {
val server = settings.getRotatorServer()
val port = settings.getRotatorPort().toInt()
val azimuth = Math.toDegrees(satPos.azimuth).round(1)
val elevation = Math.toDegrees(satPos.elevation).round(1)
dataReporter.reportRotation(server, port, azimuth, elevation)
reporter.reportRotation(server, port, azimuth, elevation)
}
_passData.postValue(RadarData(satPos, satTrack))
delay(1000)
@@ -107,7 +111,7 @@ class RadarViewModel @Inject constructor(
private fun processTransmitters(pass: SatPass) {
viewModelScope.launch {
val transmitters = dataRepository.getRadios(pass.catNum)
val transmitters = repository.getRadiosWithId(pass.catNum)
while (isActive) {
val time = System.currentTimeMillis()
val list = predictor.processRadios(pass.satellite, stationPos, transmitters, time)
@@ -7,8 +7,8 @@ import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.databinding.DialogLocationBinding
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -17,7 +17,7 @@ import javax.inject.Inject
class LocationDialog : AppCompatDialogFragment() {
@Inject
lateinit var preferences: ISettingsHandler
lateinit var preferences: ISettings
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_location, group, false)
@@ -7,8 +7,8 @@ import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.databinding.DialogLocatorBinding
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -17,7 +17,7 @@ import javax.inject.Inject
class LocatorDialog : AppCompatDialogFragment() {
@Inject
lateinit var preferences: ISettingsHandler
lateinit var preferences: ISettings
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_locator, group, false)
@@ -44,7 +44,6 @@ import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SettingsFragment : Fragment(R.layout.fragment_settings) {
private lateinit var binding: FragmentSettingsBinding
private val viewModel: SettingsViewModel by viewModels()
private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION
private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
@@ -60,15 +59,16 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
private val contentRequest = registerForActivityResult(contentContract) { uri ->
uri?.let { viewModel.updateDataFromFile(uri.toString()) }
}
private lateinit var binding: FragmentSettingsBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentSettingsBinding.bind(view)
setupViews()
setupComponents()
setupObservers()
}
private fun setupViews() {
private fun setupComponents() {
binding.settingsScroll.apply {
setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, y, _, newY ->
if (y > newY) binding.settingsFab.hide() else binding.settingsFab.show()
@@ -134,11 +134,11 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
findNavController().navigate(action)
}
binding.settingsData.dataBtnClear.setOnClickListener { viewModel.clearData() }
viewModel.entries.observe(viewLifecycleOwner) { number ->
viewModel.entriesTotal.observe(viewLifecycleOwner) { number ->
val entriesFormat = getString(R.string.fmt_entries)
binding.settingsData.dataEntries.text = String.format(entriesFormat, number)
}
viewModel.radios.observe(viewLifecycleOwner) { number ->
viewModel.radiosTotal.observe(viewLifecycleOwner) { number ->
val radiosFormat = getString(R.string.fmt_radios)
binding.settingsData.dataRadios.text = String.format(radiosFormat, number)
}
@@ -2,9 +2,9 @@ package com.rtbishop.look4sat.presentation.settingsScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.rtbishop.look4sat.data.ISettingsHandler
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationHandler
import com.rtbishop.look4sat.domain.IRepository
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -13,19 +13,20 @@ import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val settings: ISettingsHandler,
private val repository: IDataRepository,
private val locationHandler: ILocationHandler
private val locationHandler: ILocationHandler,
private val repository: IRepository,
private val settings: ISettings
) : ViewModel() {
val entries = repository.getEntriesNumber().asLiveData()
val radios = repository.getRadiosNumber().asLiveData()
val entriesTotal = repository.getEntriesTotal().asLiveData()
val radiosTotal = repository.getRadiosTotal().asLiveData()
fun updateDataFromFile(uri: String) {
repository.updateFromFile(uri)
}
fun updateDataFromWeb(sources: List<String>) {
settings.saveDataSources(sources)
repository.updateFromWeb(sources)
}
@@ -27,7 +27,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogSourcesBinding
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISettings
import com.rtbishop.look4sat.framework.model.DataSource
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
@@ -37,7 +37,7 @@ import javax.inject.Inject
class SourcesDialog : AppCompatDialogFragment(), SourcesAdapter.SourcesClickListener {
@Inject
lateinit var repository: IDataRepository
lateinit var settings: ISettings
private lateinit var binding: DialogSourcesBinding
private lateinit var sourcesAdapter: SourcesAdapter
@@ -54,7 +54,7 @@ class SourcesDialog : AppCompatDialogFragment(), SourcesAdapter.SourcesClickList
WindowManager.LayoutParams.WRAP_CONTENT
)
lifecycleScope.launchWhenResumed {
val sources = repository.getDataSources().map { url -> DataSource(url) }
val sources = settings.loadDataSources().map { url -> DataSource(url) }
val layoutManager = LinearLayoutManager(requireContext())
sourcesAdapter = SourcesAdapter(this@SourcesDialog).apply { submitList(sources) }
binding.run {
+11 -10
View File
@@ -174,20 +174,21 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<Button
android:id="@+id/radar_prev_btn"
android:id="@+id/radar_btn_notify"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_marginStart="@dimen/button_margin_side"
android:text="@string/btn_notify"
android:textColor="@color/textDisabled"
app:layout_constraintBottom_toBottomOf="@+id/radar_coordinator"
app:layout_constraintEnd_toStartOf="@+id/radar_data_name"
app:layout_constraintEnd_toStartOf="@+id/radar_sat_id"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/radar_coordinator"
app:layout_constraintStart_toStartOf="@+id/radar_coordinator"
app:layout_constraintTop_toTopOf="@+id/radar_coordinator" />
<TextView
android:id="@+id/radar_data_name"
android:id="@+id/radar_sat_id"
style="@style/WorldMapText"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -196,20 +197,20 @@
android:ellipsize="end"
android:gravity="center_horizontal"
android:maxLines="1"
android:text="@string/pat_osm_idName"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_prev_btn"
app:layout_constraintEnd_toStartOf="@+id/radar_next_btn"
android:text="@string/radar_sat_id"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_btn_notify"
app:layout_constraintEnd_toStartOf="@+id/radar_btn_settings"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/radar_prev_btn" />
app:layout_constraintStart_toEndOf="@+id/radar_btn_notify" />
<Button
android:id="@+id/radar_next_btn"
android:id="@+id/radar_btn_settings"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_marginEnd="@dimen/button_margin_side"
android:text="@string/btn_settings"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_prev_btn"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_btn_notify"
app:layout_constraintEnd_toEndOf="@+id/radar_coordinator"
app:layout_constraintStart_toEndOf="@+id/radar_data_name" />
app:layout_constraintStart_toEndOf="@+id/radar_sat_id" />
</androidx.constraintlayout.widget.ConstraintLayout>
+1 -1
View File
@@ -6,7 +6,7 @@
android:id="@+id/mode_checkbox"
android:layout_width="match_parent"
android:layout_height="41dp"
android:layout_marginStart="4dp"
android:layout_marginStart="6dp"
android:background="@color/cardRegular"
android:ellipsize="end"
android:lines="1"
+2
View File
@@ -72,6 +72,8 @@
<string name="radar_alt_value">%.0f km</string>
<string name="radar_dist_text">Дальн.</string>
<string name="radar_dist_value">%.0f km</string>
<string name="radar_sat_id">Id: %d</string>
<string name="radar_sat_name">%s</string>
<string name="pat_azimuth">Азимут: %.1f°</string>
<string name="pat_elevation">Элевация: %.1f°</string>
+2
View File
@@ -84,6 +84,8 @@
<string name="radar_alt_value">%.0f km</string>
<string name="radar_dist_text">Distance</string>
<string name="radar_dist_value">%.0f km</string>
<string name="radar_sat_id">Id: %d</string>
<string name="radar_sat_name">%s</string>
<string name="pat_azimuth">Azimuth: %.1f°</string>
<string name="pat_elevation">Elevation: %.1f°</string>
@@ -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.domain
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.OrbitalData
@@ -19,9 +19,11 @@ package com.rtbishop.look4sat.data
import java.io.InputStream
interface IRemoteSource {
interface IProvider {
val radioApi: String get() = "https://db.satnogs.org/api/transmitters/?format=json"
suspend fun getFileStream(url: String): InputStream?
suspend fun getLocalFileStream(uri: String): InputStream?
suspend fun getRemoteFileStream(url: String): InputStream?
}
@@ -22,33 +22,22 @@ import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import kotlinx.coroutines.flow.Flow
import java.io.InputStream
interface ILocalSource {
interface IStorage {
fun getEntriesNumber(): Flow<Int>
fun getEntriesTotal(): Flow<Int>
fun getRadiosNumber(): Flow<Int>
fun getRadiosTotal(): Flow<Int>
suspend fun getEntriesWithModes(): List<SatItem>
suspend fun getSelectedEntries(): List<Satellite>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
suspend fun getRadios(catnum: Int): List<SatRadio>
suspend fun getFileStream(uri: String): InputStream?
suspend fun getRadiosWithId(id: Int): List<SatRadio>
suspend fun insertEntries(entries: List<SatEntry>)
suspend fun insertRadios(radios: List<SatRadio>)
suspend fun clearAllData()
suspend fun getDataSources(): List<String>
suspend fun setDataSources(sources: List<String>)
suspend fun getEntriesSelection(): List<Int>
suspend fun setEntriesSelection(catnums: List<Int>)
}
@@ -17,8 +17,7 @@
*/
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.DataParser
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.IRepository
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatEntry
import kotlinx.coroutines.*
@@ -27,12 +26,12 @@ import kotlinx.coroutines.flow.StateFlow
import java.io.InputStream
import java.util.zip.ZipInputStream
class DataRepository(
class Repository(
private val dataParser: DataParser,
private val localSource: ILocalSource,
private val remoteSource: IRemoteSource,
private val repositoryScope: CoroutineScope
) : IDataRepository {
private val storage: IStorage,
private val provider: IProvider,
private val repoScope: CoroutineScope
) : IRepository {
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
_updateState.value = DataState.Error(exception.message)
@@ -41,42 +40,35 @@ class DataRepository(
private val _updateState = MutableStateFlow<DataState<Long>>(DataState.Handled)
override val updateState: StateFlow<DataState<Long>> = _updateState
override fun setUpdateStateHandled() {
_updateState.value = DataState.Handled
}
override fun getEntriesTotal() = storage.getEntriesTotal()
override fun getEntriesNumber() = localSource.getEntriesNumber()
override fun getRadiosTotal() = storage.getRadiosTotal()
override fun getRadiosNumber() = localSource.getRadiosNumber()
override suspend fun getEntriesWithModes() = storage.getEntriesWithModes()
override suspend fun getEntriesWithModes() = localSource.getEntriesWithModes()
override suspend fun getEntriesWithIds(ids: List<Int>) = storage.getEntriesWithIds(ids)
override suspend fun getSelectedEntries() = localSource.getSelectedEntries()
override suspend fun getRadios(catnum: Int) = localSource.getRadios(catnum)
override suspend fun getRadiosWithId(id: Int) = storage.getRadiosWithId(id)
override fun updateFromFile(uri: String) {
repositoryScope.launch(exceptionHandler) {
repoScope.launch(exceptionHandler) {
_updateState.value = DataState.Loading
localSource.getFileStream(uri)?.let { fileStream ->
provider.getLocalFileStream(uri)?.let { fileStream ->
delay(updateStateDelay)
localSource.insertEntries(importSatellites(fileStream))
storage.insertEntries(importSatellites(fileStream))
}
_updateState.value = DataState.Success(0L)
}
}
override fun updateFromWeb(sources: List<String>) {
override fun updateFromWeb(urls: List<String>) {
_updateState.value = DataState.Loading
repositoryScope.launch(exceptionHandler) {
localSource.setDataSources(sources)
}
repositoryScope.launch(exceptionHandler) {
repoScope.launch(exceptionHandler) {
val jobsMap = mutableMapOf<String, Deferred<InputStream?>>()
val streamsMap = mutableMapOf<String, InputStream?>()
val streams = mutableListOf<InputStream>()
val entries = mutableListOf<SatEntry>()
sources.forEach { jobsMap[it] = async { remoteSource.getFileStream(it) } }
urls.forEach { jobsMap[it] = async { provider.getRemoteFileStream(it) } }
jobsMap.forEach { job -> streamsMap[job.key] = job.value.await() }
streamsMap.forEach { stream ->
stream.value?.let { inputStream ->
@@ -93,29 +85,27 @@ class DataRepository(
}
}
streams.forEach { stream -> entries.addAll(importSatellites(stream)) }
localSource.insertEntries(entries)
storage.insertEntries(entries)
}
repositoryScope.launch(exceptionHandler) {
remoteSource.getFileStream(remoteSource.radioApi)?.let { stream ->
localSource.insertRadios(dataParser.parseJSONStream(stream))
repoScope.launch(exceptionHandler) {
provider.getRemoteFileStream(provider.radioApi)?.let { stream ->
storage.insertRadios(dataParser.parseJSONStream(stream))
_updateState.value = DataState.Success(0L)
}
}
}
override fun clearAllData() {
repositoryScope.launch {
_updateState.value = DataState.Loading
delay(updateStateDelay)
localSource.clearAllData()
_updateState.value = DataState.Success(0L)
}
override fun setUpdateStateHandled() {
_updateState.value = DataState.Handled
}
override suspend fun getDataSources() = localSource.getDataSources()
override suspend fun setEntriesSelection(catnums: List<Int>) {
localSource.setEntriesSelection(catnums)
override fun clearAllData() {
repoScope.launch {
_updateState.value = DataState.Loading
delay(updateStateDelay)
storage.clearAllData()
_updateState.value = DataState.Success(0L)
}
}
private suspend fun importSatellites(stream: InputStream): List<SatEntry> {
@@ -24,29 +24,25 @@ import com.rtbishop.look4sat.domain.predict.Satellite
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface IDataRepository {
interface IRepository {
val updateState: StateFlow<DataState<Long>>
fun setUpdateStateHandled()
fun getEntriesTotal(): Flow<Int>
fun getEntriesNumber(): Flow<Int>
fun getRadiosNumber(): Flow<Int>
fun getRadiosTotal(): Flow<Int>
suspend fun getEntriesWithModes(): List<SatItem>
suspend fun getSelectedEntries(): List<Satellite>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
suspend fun getRadios(catnum: Int): List<SatRadio>
suspend fun getRadiosWithId(id: Int): List<SatRadio>
fun updateFromFile(uri: String)
fun updateFromWeb(sources: List<String>)
fun updateFromWeb(urls: List<String>)
fun setUpdateStateHandled()
fun clearAllData()
suspend fun getDataSources(): List<String>
suspend fun setEntriesSelection(catnums: List<Int>)
}
@@ -15,11 +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.data
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.predict.GeoPos
interface ISettingsHandler {
interface ISettings {
val defaultSources: List<String>
get() = listOf(
@@ -17,7 +17,7 @@
*/
package com.rtbishop.look4sat
import com.rtbishop.look4sat.domain.DataParser
import com.rtbishop.look4sat.data.DataParser
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest