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