Moved sources urls to database storage

This commit is contained in:
Arty Bishop committed 2021-10-27 21:47:02 +01:00
1 parent af7e027350
commit 5f4c63ee49
27 files changed
+403 -122

No files matched your search

@@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 2, "version": 2,
"identityHash": "825f728737780384e51a6a73221288c4", "identityHash": "555494c464b5a29285eb5493cc8aa5f6",
"entities": [ "entities": [
{ {
"tableName": "entries", "tableName": "entries",
@@ -103,12 +103,32 @@
}, },
"indices": [], "indices": [],
"foreignKeys": [] "foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, PRIMARY KEY(`sourceUrl`))",
"fields": [
{
"fieldPath": "sourceUrl",
"columnName": "sourceUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"sourceUrl"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
} }
], ],
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '825f728737780384e51a6a73221288c4')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '555494c464b5a29285eb5493cc8aa5f6')"
] ]
} }
} }
@@ -0,0 +1,134 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "555494c464b5a29285eb5493cc8aa5f6",
"entities": [
{
"tableName": "entries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tle` TEXT NOT NULL, `catNum` INTEGER NOT NULL, `name` TEXT NOT NULL, `isSelected` INTEGER NOT NULL, PRIMARY KEY(`catNum`))",
"fields": [
{
"fieldPath": "tle",
"columnName": "tle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "catNum",
"columnName": "catNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isSelected",
"columnName": "isSelected",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"catNum"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "transmitters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catNum` INTEGER, PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "info",
"columnName": "info",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlive",
"columnName": "isAlive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downlink",
"columnName": "downlink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uplink",
"columnName": "uplink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isInverted",
"columnName": "isInverted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "catNum",
"columnName": "catNum",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uuid"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, PRIMARY KEY(`sourceUrl`))",
"fields": [
{
"fieldPath": "sourceUrl",
"columnName": "sourceUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"sourceUrl"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '555494c464b5a29285eb5493cc8aa5f6')"
]
}
}
@@ -0,0 +1,75 @@
package com.rtbishop.look4sat.framework
import android.Manifest
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.hardware.GeomagneticField
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.domain.LocationProvider
import com.rtbishop.look4sat.domain.QthConverter
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.utility.round
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationSource @Inject constructor(
@ApplicationContext private val context: Context,
private val preferences: SharedPreferences
) : LocationListener, LocationProvider {
private val locManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val _updatedLocation = MutableSharedFlow<GeoPos?>()
override val updatedLocation: SharedFlow<GeoPos?> = _updatedLocation
fun getMagDeclination(stationPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val lat = stationPos.latitude.toFloat()
val lon = stationPos.longitude.toFloat()
return GeomagneticField(lat, lon, 0f, time).declination
}
fun updatePosition(latitude: Double, longitude: Double) {
if (QthConverter.isValidPosition(latitude, longitude)) {
_updatedLocation.tryEmit(GeoPos(latitude, longitude))
} else _updatedLocation.tryEmit(null)
}
fun updatePositionFromQth(qthString: String) {
val position = QthConverter.qthToPosition(qthString)
if (position != null) {
_updatedLocation.tryEmit(GeoPos(position.latitude, position.longitude))
} else _updatedLocation.tryEmit(null)
}
fun updatePositionFromGps() {
val provider = LocationManager.GPS_PROVIDER
val permission = Manifest.permission.ACCESS_FINE_LOCATION
val result = ContextCompat.checkSelfPermission(context, permission)
if (locManager.isProviderEnabled(provider) && result == PackageManager.PERMISSION_GRANTED) {
locManager.requestLocationUpdates(provider, 0L, 0f, this)
} else _updatedLocation.tryEmit(null)
}
fun updatePositionFromNetwork() {
val provider = LocationManager.NETWORK_PROVIDER
val permission = Manifest.permission.ACCESS_COARSE_LOCATION
val result = ContextCompat.checkSelfPermission(context, permission)
if (locManager.isProviderEnabled(provider) && result == PackageManager.PERMISSION_GRANTED) {
locManager.requestLocationUpdates(provider, 0L, 0f, this)
} else _updatedLocation.tryEmit(null)
}
override fun onLocationChanged(location: Location) {
locManager.removeUpdates(this)
val latitude = location.latitude.round(4)
val longitude = location.longitude.round(4)
_updatedLocation.tryEmit(GeoPos(latitude, longitude))
}
}
@@ -22,8 +22,10 @@ import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
import android.hardware.SensorManager import android.hardware.SensorManager
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.round import kotlin.math.round
@Singleton
class OrientationSource @Inject constructor(private val sensorManager: SensorManager) : class OrientationSource @Inject constructor(private val sensorManager: SensorManager) :
SensorEventListener { SensorEventListener {
@@ -21,50 +21,19 @@ import android.content.SharedPreferences
import android.hardware.GeomagneticField import android.hardware.GeomagneticField
import android.location.LocationManager import android.location.LocationManager
import androidx.core.content.edit import androidx.core.content.edit
import com.rtbishop.look4sat.domain.Constants
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.QthConverter import com.rtbishop.look4sat.domain.QthConverter
import com.rtbishop.look4sat.utility.round import com.rtbishop.look4sat.utility.round
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PreferencesSource @Inject constructor( class PreferencesSource @Inject constructor(
moshi: Moshi,
private val locationManager: LocationManager, private val locationManager: LocationManager,
private val preferences: SharedPreferences private val preferences: SharedPreferences
) { ) {
private val sourcesType = Types.newParameterizedType(List::class.java, String::class.java)
private val sourcesAdapter = moshi.adapter<List<String>>(sourcesType)
fun loadTleSources(): List<String> {
return try {
val sourcesString = preferences.getString(keySources, String())
if (sourcesString.isNullOrEmpty()) {
loadDefaultSources()
} else {
sourcesAdapter.fromJson(sourcesString) ?: loadDefaultSources()
}
} catch (exception: Exception) {
loadDefaultSources()
}
}
fun saveTleSources(sources: List<String>) {
val sourcesJson = sourcesAdapter.toJson(sources)
preferences.edit { putString(keySources, sourcesJson) }
}
fun loadDefaultSources(): List<String> {
return listOf(
Constants.URL_CELESTRAK, Constants.URL_AMSAT,
Constants.URL_PRISM_CLASSFD, Constants.URL_PRISM_INTEL
)
}
companion object { companion object {
const val keySources = "prefTleSourcesKey"
const val keyModes = "satModes" const val keyModes = "satModes"
const val keyCompass = "compass" const val keyCompass = "compass"
const val keyRadarSweep = "radarSweep" const val keyRadarSweep = "radarSweep"
@@ -78,7 +47,6 @@ class PreferencesSource @Inject constructor(
const val keyRotatorPort = "rotatorPort" const val keyRotatorPort = "rotatorPort"
const val keyLatitude = "stationLat" const val keyLatitude = "stationLat"
const val keyLongitude = "stationLon" const val keyLongitude = "stationLon"
const val keyAltitude = "stationAlt"
const val keyPositionGPS = "setPositionGPS" const val keyPositionGPS = "setPositionGPS"
const val keyPositionQTH = "setPositionQTH" const val keyPositionQTH = "setPositionQTH"
} }
@@ -87,15 +55,13 @@ class PreferencesSource @Inject constructor(
val defaultSP = "0.0" val defaultSP = "0.0"
val latitude = preferences.getString(keyLatitude, null) ?: defaultSP val latitude = preferences.getString(keyLatitude, null) ?: defaultSP
val longitude = preferences.getString(keyLongitude, null) ?: defaultSP val longitude = preferences.getString(keyLongitude, null) ?: defaultSP
val altitude = preferences.getString(keyAltitude, null) ?: defaultSP return GeoPos(latitude.toDouble(), longitude.toDouble())
return GeoPos(latitude.toDouble(), longitude.toDouble(), altitude.toDouble())
} }
fun saveStationPosition(pos: GeoPos) { fun saveStationPosition(pos: GeoPos) {
preferences.edit { preferences.edit {
putString(keyLatitude, pos.latitude.toString()) putString(keyLatitude, pos.latitude.toString())
putString(keyLongitude, pos.longitude.toString()) putString(keyLongitude, pos.longitude.toString())
putString(keyAltitude, pos.altitude.toString())
} }
} }
@@ -106,8 +72,7 @@ class PreferencesSource @Inject constructor(
else { else {
val latitude = location.latitude.round(4) val latitude = location.latitude.round(4)
val longitude = location.longitude.round(4) val longitude = location.longitude.round(4)
val altitude = location.altitude.round(1) val stationPosition = GeoPos(latitude, longitude)
val stationPosition = GeoPos(latitude, longitude, altitude)
saveStationPosition(stationPosition) saveStationPosition(stationPosition)
return true return true
} }
@@ -118,7 +83,7 @@ class PreferencesSource @Inject constructor(
fun updatePositionFromQTH(qthString: String): Boolean { fun updatePositionFromQTH(qthString: String): Boolean {
val position = QthConverter.qthToPosition(qthString) ?: return false val position = QthConverter.qthToPosition(qthString) ?: return false
val stationPosition = GeoPos(position.latitude, position.longitude, 0.0) val stationPosition = GeoPos(position.latitude, position.longitude)
saveStationPosition(stationPosition) saveStationPosition(stationPosition)
return true return true
} }
@@ -127,8 +92,7 @@ class PreferencesSource @Inject constructor(
val stationPosition = loadStationPosition() val stationPosition = loadStationPosition()
val lat = stationPosition.latitude.toFloat() val lat = stationPosition.latitude.toFloat()
val lon = stationPosition.longitude.toFloat() val lon = stationPosition.longitude.toFloat()
val alt = stationPosition.altitude.toFloat() return GeomagneticField(lat, lon, 0f, System.currentTimeMillis()).declination
return GeomagneticField(lat, lon, alt, System.currentTimeMillis()).declination
} }
fun getHoursAhead(): Int { fun getHoursAhead(): Int {
@@ -23,16 +23,24 @@ import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.model.Transmitter import com.rtbishop.look4sat.domain.model.Transmitter
import com.rtbishop.look4sat.framework.DataMapper import com.rtbishop.look4sat.framework.DataMapper
import com.rtbishop.look4sat.framework.model.DataSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class LocalSource(private val satelliteDao: SatelliteDao) : LocalDataSource { class LocalSource(
private val satelliteDao: SatelliteDao,
private val sourcesDao: SourcesDao
) : LocalDataSource {
override fun getEntriesWithModes(): Flow<List<SatItem>> { override fun getEntriesWithModes(): Flow<List<SatItem>> {
return satelliteDao.getSatItems() return satelliteDao.getSatItems()
.map { satItems -> DataMapper.satItemsToDomainItems(satItems) } .map { satItems -> DataMapper.satItemsToDomainItems(satItems) }
} }
override suspend fun getSources(): List<String> {
return sourcesDao.getSources()
}
override suspend fun getSelectedSatellites(): List<Satellite> { override suspend fun getSelectedSatellites(): List<Satellite> {
return satelliteDao.getSelectedSatellites() return satelliteDao.getSelectedSatellites()
} }
@@ -46,6 +54,11 @@ class LocalSource(private val satelliteDao: SatelliteDao) : LocalDataSource {
satelliteDao.updateEntriesSelection(catNums, isSelected) satelliteDao.updateEntriesSelection(catNums, isSelected)
} }
override suspend fun updateSources(sources: List<String>) {
sourcesDao.deleteSources()
sourcesDao.setSources(sources.map { DataSource(it) })
}
override fun getTransmitters(catNum: Int): Flow<List<Transmitter>> { override fun getTransmitters(catNum: Int): Flow<List<Transmitter>> {
return satelliteDao.getSatTransmitters(catNum) return satelliteDao.getSatTransmitters(catNum)
.map { satTransList -> DataMapper.satTransListToDomainTransList(satTransList) } .map { satTransList -> DataMapper.satTransListToDomainTransList(satTransList) }
@@ -22,14 +22,21 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.rtbishop.look4sat.framework.model.DataSource
import com.rtbishop.look4sat.framework.model.SatEntry import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.Transmitter import com.rtbishop.look4sat.framework.model.Transmitter
@Database(entities = [SatEntry::class, Transmitter::class], version = 2, exportSchema = true) @Database(
entities = [SatEntry::class, Transmitter::class, DataSource::class],
version = 3,
exportSchema = true
)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class SatelliteDb : RoomDatabase() { abstract class SatelliteDb : RoomDatabase() {
abstract fun satelliteDao(): SatelliteDao abstract fun satelliteDao(): SatelliteDao
abstract fun sourcesDao(): SourcesDao
} }
val MIGRATION_1_2 = object : Migration(1, 2) { val MIGRATION_1_2 = object : Migration(1, 2) {
@@ -40,3 +47,9 @@ val MIGRATION_1_2 = object : Migration(1, 2) {
database.execSQL("ALTER TABLE trans_backup RENAME TO transmitters") database.execSQL("ALTER TABLE trans_backup RENAME TO transmitters")
} }
} }
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE sources (sourceUrl TEXT NOT NULL, PRIMARY KEY(sourceUrl))")
}
}
@@ -0,0 +1,21 @@
package com.rtbishop.look4sat.framework.local
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.rtbishop.look4sat.framework.model.DataSource
import kotlinx.coroutines.flow.Flow
@Dao
interface SourcesDao {
@Query("SELECT sourceUrl FROM sources")
suspend fun getSources(): List<String>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun setSources(sources: List<DataSource>)
@Query("DELETE from sources")
suspend fun deleteSources()
}
@@ -15,6 +15,10 @@
* 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.presentation.sourcesScreen package com.rtbishop.look4sat.framework.model
class DataSource(var url: String = String()) import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "sources")
data class DataSource(@PrimaryKey var sourceUrl: String = String())
@@ -24,12 +24,8 @@ import android.hardware.SensorManager
import android.location.LocationManager import android.location.LocationManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.room.Room import androidx.room.Room
import com.rtbishop.look4sat.domain.Constants
import com.rtbishop.look4sat.framework.PreferencesSource import com.rtbishop.look4sat.framework.PreferencesSource
import com.rtbishop.look4sat.framework.local.Converters import com.rtbishop.look4sat.framework.local.*
import com.rtbishop.look4sat.framework.local.MIGRATION_1_2
import com.rtbishop.look4sat.framework.local.SatelliteDao
import com.rtbishop.look4sat.framework.local.SatelliteDb
import com.rtbishop.look4sat.framework.remote.SatelliteApi import com.rtbishop.look4sat.framework.remote.SatelliteApi
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.Module import dagger.Module
@@ -74,18 +70,17 @@ object AppModule {
@Provides @Provides
@Singleton @Singleton
fun providePreferenceSource( fun providePreferenceSource(
moshi: Moshi,
locationManager: LocationManager, locationManager: LocationManager,
preferences: SharedPreferences preferences: SharedPreferences
): PreferencesSource { ): PreferencesSource {
return PreferencesSource(moshi, locationManager, preferences) return PreferencesSource(locationManager, preferences)
} }
@Provides @Provides
@Singleton @Singleton
fun provideSatelliteApi(): SatelliteApi { fun provideSatelliteApi(): SatelliteApi {
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(Constants.URL_BASE) .baseUrl("https://db.satnogs.org/api/")
.addConverterFactory(MoshiConverterFactory.create()) .addConverterFactory(MoshiConverterFactory.create())
.build().create(SatelliteApi::class.java) .build().create(SatelliteApi::class.java)
} }
@@ -96,11 +91,17 @@ object AppModule {
return db.satelliteDao() return db.satelliteDao()
} }
@Provides
@Singleton
fun provideSourcesDao(db: SatelliteDb): SourcesDao {
return db.sourcesDao()
}
@Provides @Provides
@Singleton @Singleton
fun provideSatelliteDb(@ApplicationContext context: Context, moshi: Moshi): SatelliteDb { fun provideSatelliteDb(@ApplicationContext context: Context, moshi: Moshi): SatelliteDb {
Converters.initialize(moshi) Converters.initialize(moshi)
return Room.databaseBuilder(context, SatelliteDb::class.java, "SatelliteDb") return Room.databaseBuilder(context, SatelliteDb::class.java, "SatelliteDb")
.addMigrations(MIGRATION_1_2).build() .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
} }
} }
@@ -38,8 +38,11 @@ import javax.inject.Singleton
object CoreModule { object CoreModule {
@Provides @Provides
fun provideLocalDataSource(satelliteDao: SatelliteDao): LocalDataSource { fun provideLocalDataSource(
return LocalSource(satelliteDao) satelliteDao: SatelliteDao,
sourcesDao: SourcesDao
): LocalDataSource {
return LocalSource(satelliteDao, sourcesDao)
} }
@Provides @Provides
@@ -37,7 +37,7 @@ import javax.inject.Inject
class EntriesViewModel @Inject constructor( class EntriesViewModel @Inject constructor(
private val preferences: PreferencesSource, private val preferences: PreferencesSource,
private val resolver: ContentResolver, private val resolver: ContentResolver,
private val satelliteRepo: SatelliteRepo, private val satelliteRepo: SatelliteRepo
) : ViewModel(), SearchView.OnQueryTextListener { ) : ViewModel(), SearchView.OnQueryTextListener {
private val coroutineHandler = CoroutineExceptionHandler { _, throwable -> private val coroutineHandler = CoroutineExceptionHandler { _, throwable ->
@@ -72,7 +72,6 @@ class EntriesViewModel @Inject constructor(
fun updateEntriesFromWeb(sources: List<String>) { fun updateEntriesFromWeb(sources: List<String>) {
viewModelScope.launch(coroutineHandler) { viewModelScope.launch(coroutineHandler) {
_satData.value = DataState.Loading _satData.value = DataState.Loading
preferences.saveTleSources(sources)
satelliteRepo.updateEntriesFromWeb(sources) satelliteRepo.updateEntriesFromWeb(sources)
} }
} }
@@ -41,9 +41,14 @@ import java.util.*
class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesClickListener { class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesClickListener {
private val passesViewModel: PassesViewModel by viewModels() private val passesViewModel: PassesViewModel by viewModels()
private val permRequest = ActivityResultContracts.RequestPermission() private val permReqContract = ActivityResultContracts.RequestMultiplePermissions()
private val permRequestLauncher = registerForActivityResult(permRequest) { private val locPermFine = Manifest.permission.ACCESS_FINE_LOCATION
passesViewModel.triggerInitialSetup() private val locPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
private val locPermReq = registerForActivityResult(permReqContract) { permissions ->
when {
permissions[locPermFine] == true -> passesViewModel.triggerInitialSetup()
permissions[locPermCoarse] == true -> passesViewModel.triggerInitialSetup()
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -68,7 +73,7 @@ class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesC
handleNewPasses(passesResult, passesAdapter, binding) handleNewPasses(passesResult, passesAdapter, binding)
}) })
passesViewModel.isFirstLaunchDone.observe(viewLifecycleOwner, { setupDone -> passesViewModel.isFirstLaunchDone.observe(viewLifecycleOwner, { setupDone ->
if (!setupDone) permRequestLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) if (!setupDone) locPermReq.launch(arrayOf(locPermFine, locPermCoarse))
}) })
} }
@@ -95,7 +100,7 @@ class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesC
passesProgress.visibility = View.VISIBLE passesProgress.visibility = View.VISIBLE
} }
} }
is DataState.Error -> { else -> {
binding.apply { binding.apply {
passesTimer.text = 0L.toTimerString() passesTimer.text = 0L.toTimerString()
passesProgress.visibility = View.INVISIBLE passesProgress.visibility = View.INVISIBLE
@@ -77,7 +77,7 @@ class PassesViewModel @Inject constructor(
val stationPos = preferences.loadStationPosition() val stationPos = preferences.loadStationPosition()
val hoursAhead = preferences.getHoursAhead() val hoursAhead = preferences.getHoursAhead()
val minElev = preferences.getMinElevation() val minElev = preferences.getMinElevation()
satelliteRepo.updateEntriesFromWeb(preferences.loadDefaultSources()) satelliteRepo.updateEntriesFromWeb(satelliteRepo.getDefaultSources())
satelliteRepo.updateEntriesSelection(defaultCatNums, true) satelliteRepo.updateEntriesSelection(defaultCatNums, true)
predictor.forceCalculation(satellites, stationPos, dateNow, hoursAhead, minElev) predictor.forceCalculation(satellites, stationPos, dateNow, hoursAhead, minElev)
preferences.setSetupDone() preferences.setSetupDone()
@@ -39,12 +39,14 @@ class SettingsFragment : PreferenceFragmentCompat() {
@Inject @Inject
lateinit var preferences: PreferencesSource lateinit var preferences: PreferencesSource
private val requestPermissionLauncher = private val permReqContract = ActivityResultContracts.RequestMultiplePermissions()
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> private val locPermFine = Manifest.permission.ACCESS_FINE_LOCATION
if (isGranted) { private val locPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
updatePositionFromGPS() private val locPermReq = registerForActivityResult(permReqContract) { permissions ->
} else { when {
showSnack(getString(R.string.pref_pos_gps_error)) permissions[locPermFine] == true -> updatePositionFromGPS()
permissions[locPermCoarse] == true -> updatePositionFromGPS()
else -> showSnack(getString(R.string.pref_pos_gps_error))
} }
} }
@@ -113,7 +115,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
} else { } else {
showSnack(getString(R.string.pref_pos_gps_null)) showSnack(getString(R.string.pref_pos_gps_null))
} }
} else requestPermissionLauncher.launch(locPermString) } else locPermReq.launch(arrayOf(locPermFine, locPermCoarse))
} }
private fun showSnack(message: String) { private fun showSnack(message: String) {
@@ -22,12 +22,13 @@ import android.view.ViewGroup
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.databinding.ItemSourceBinding import com.rtbishop.look4sat.databinding.ItemSourceBinding
import com.rtbishop.look4sat.framework.model.DataSource
class SourcesAdapter(private val sources: MutableList<DataSource> = mutableListOf()) : class SourcesAdapter(private val sources: MutableList<DataSource> = mutableListOf()) :
RecyclerView.Adapter<SourcesAdapter.TleSourceHolder>() { RecyclerView.Adapter<SourcesAdapter.TleSourceHolder>() {
fun getSources(): List<DataSource> { fun getSources(): List<DataSource> {
return sources.filter { it.url.contains("https://") } return sources.filter { it.sourceUrl.contains("https://") }
} }
fun setSources(list: List<DataSource>) { fun setSources(list: List<DataSource>) {
@@ -58,8 +59,8 @@ class SourcesAdapter(private val sources: MutableList<DataSource> = mutableListO
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(source: DataSource) { fun bind(source: DataSource) {
binding.sourceUrl.setText(source.url) binding.sourceUrl.setText(source.sourceUrl)
binding.sourceUrl.doOnTextChanged { text, _, _, _ -> source.url = text.toString() } binding.sourceUrl.doOnTextChanged { text, _, _, _ -> source.sourceUrl = text.toString() }
binding.sourceInput.setEndIconOnClickListener { binding.sourceInput.setEndIconOnClickListener {
sources.remove(source) sources.remove(source)
notifyItemRemoved(adapterPosition) notifyItemRemoved(adapterPosition)
@@ -23,19 +23,18 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.viewModels
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.framework.PreferencesSource import com.rtbishop.look4sat.framework.model.DataSource
import com.rtbishop.look4sat.utility.setNavResult import com.rtbishop.look4sat.utility.setNavResult
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SourcesDialog : AppCompatDialogFragment() { class SourcesDialog : AppCompatDialogFragment() {
@Inject private val viewModel: SourcesViewModel by viewModels()
lateinit var prefsManager: PreferencesSource
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_sources, group, false) return inflater.inflate(R.layout.dialog_sources, group, false)
@@ -43,25 +42,26 @@ class SourcesDialog : AppCompatDialogFragment() {
override fun onViewCreated(view: View, state: Bundle?) { override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state) super.onViewCreated(view, state)
val sources = prefsManager.loadTleSources().map { DataSource(it) } viewModel.sources.observe(viewLifecycleOwner, { sources ->
val sourcesAdapter = SourcesAdapter().apply { setSources(sources) } val adapter = SourcesAdapter().apply { setSources(sources.map { DataSource(it) }) }
DialogSourcesBinding.bind(view).apply { DialogSourcesBinding.bind(view).apply {
dialog?.window?.setLayout( dialog?.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT WindowManager.LayoutParams.WRAP_CONTENT
) )
sourcesRecycler.apply { sourcesRecycler.apply {
adapter = sourcesAdapter this.adapter = adapter
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())
} }
sourcesBtnAdd.setOnClickListener { sourcesBtnAdd.setOnClickListener {
sourcesAdapter.addSource() adapter.addSource()
} }
sourcesBtnPos.setOnClickListener { sourcesBtnPos.setOnClickListener {
setNavResult("sources", sourcesAdapter.getSources().map { it.url }) setNavResult("sources", adapter.getSources().map { it.sourceUrl })
dismiss() dismiss()
} }
sourcesBtnNeg.setOnClickListener { dismiss() } sourcesBtnNeg.setOnClickListener { dismiss() }
} }
})
} }
} }
@@ -0,0 +1,15 @@
package com.rtbishop.look4sat.presentation.sourcesScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.rtbishop.look4sat.domain.SatelliteRepo
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class SourcesViewModel @Inject constructor(satelliteRepo: SatelliteRepo) : ViewModel() {
val sources = liveData {
emit(satelliteRepo.getSavedSources())
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
buildscript { buildscript {
ext { ext {
gradle_version = '7.0.2' gradle_version = '7.0.3'
kotlin_version = '1.5.31' kotlin_version = '1.5.31'
coroutines_version = '1.5.2-native-mt' coroutines_version = '1.5.2-native-mt'
material_version = '1.4.0' material_version = '1.4.0'
@@ -15,7 +15,7 @@ buildscript {
osmdroid_version = '6.1.11' osmdroid_version = '6.1.11'
timber_version = '5.0.1' timber_version = '5.0.1'
junit_version = '4.13.2' junit_version = '4.13.2'
mockito_version = '3.12.4' mockito_version = '4.0.0'
leak_canary_version = '2.7' leak_canary_version = '2.7'
} }
repositories { repositories {
@@ -45,6 +45,22 @@ class DataRepository(
return localSource.getTransmitters(catNum) return localSource.getTransmitters(catNum)
} }
override fun getDefaultSources(): List<String> {
return listOf(
"https://celestrak.com/NORAD/elements/active.txt",
"https://amsat.org/tle/current/nasabare.txt",
"https://www.prismnet.com/~mmccants/tles/classfd.zip",
"https://www.prismnet.com/~mmccants/tles/inttles.zip"
)
}
override suspend fun getSavedSources(): List<String> {
val savedSources = localSource.getSources()
return if (savedSources.isEmpty()) {
getDefaultSources()
} else savedSources
}
override suspend fun getSelectedSatellites(): List<Satellite> { override suspend fun getSelectedSatellites(): List<Satellite> {
return localSource.getSelectedSatellites() return localSource.getSelectedSatellites()
} }
@@ -55,6 +71,9 @@ class DataRepository(
override suspend fun updateEntriesFromWeb(sources: List<String>) { override suspend fun updateEntriesFromWeb(sources: List<String>) {
coroutineScope { coroutineScope {
launch(repoDispatcher) {
localSource.updateSources(sources)
}
launch(repoDispatcher) { launch(repoDispatcher) {
val streams = mutableListOf<InputStream>() val streams = mutableListOf<InputStream>()
val entries = mutableListOf<SatEntry>() val entries = mutableListOf<SatEntry>()
@@ -29,11 +29,15 @@ interface LocalDataSource {
fun getTransmitters(catNum: Int): Flow<List<Transmitter>> fun getTransmitters(catNum: Int): Flow<List<Transmitter>>
suspend fun getSources(): List<String>
suspend fun getSelectedSatellites(): List<Satellite> suspend fun getSelectedSatellites(): List<Satellite>
suspend fun updateEntries(entries: List<SatEntry>) suspend fun updateEntries(entries: List<SatEntry>)
suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean) suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean)
suspend fun updateSources(sources: List<String>)
suspend fun updateTransmitters(transmitters: List<Transmitter>) suspend fun updateTransmitters(transmitters: List<Transmitter>)
} }
@@ -1,27 +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.domain
object Constants {
const val URL_BASE = "https://db.satnogs.org/api/"
const val URL_CELESTRAK = "https://celestrak.com/NORAD/elements/active.txt"
const val URL_AMSAT = "https://amsat.org/tle/current/nasabare.txt"
const val URL_PRISM_CLASSFD = "https://www.prismnet.com/~mmccants/tles/classfd.zip"
const val URL_PRISM_INTEL = "https://www.prismnet.com/~mmccants/tles/inttles.zip"
}
@@ -0,0 +1,9 @@
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.predict.GeoPos
import kotlinx.coroutines.flow.SharedFlow
interface LocationProvider {
val updatedLocation: SharedFlow<GeoPos?>
}
@@ -52,15 +52,15 @@ object QthConverter {
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird" return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
} }
fun isValidPosition(lat: Double, lon: Double): Boolean {
return (lat > -90.0 && lat < 90.0) && (lon > -180.0 && lon < 360.0)
}
private fun isValidQth(qthString: String): Boolean { private fun isValidQth(qthString: String): Boolean {
val qthPattern = "[a-xA-X][a-xA-X][0-9][0-9][a-xA-X][a-xA-X]".toRegex() val qthPattern = "[a-xA-X][a-xA-X][0-9][0-9][a-xA-X][a-xA-X]".toRegex()
return qthString.matches(qthPattern) return qthString.matches(qthPattern)
} }
private fun isValidPosition(lat: Double, lon: Double): Boolean {
return (lat > -90.0 && lat < 90.0) && (lon > -180.0 && lon < 360.0)
}
private fun Double.roundToDecimals(decimals: Int): Double { private fun Double.roundToDecimals(decimals: Int): Double {
var multiplier = 1.0 var multiplier = 1.0
repeat(decimals) { multiplier *= 10 } repeat(decimals) { multiplier *= 10 }
@@ -29,6 +29,10 @@ interface SatelliteRepo {
fun getSatTransmitters(catNum: Int): Flow<List<Transmitter>> fun getSatTransmitters(catNum: Int): Flow<List<Transmitter>>
fun getDefaultSources(): List<String>
suspend fun getSavedSources(): List<String>
suspend fun getSelectedSatellites(): List<Satellite> suspend fun getSelectedSatellites(): List<Satellite>
suspend fun updateEntriesFromFile(stream: InputStream) suspend fun updateEntriesFromFile(stream: InputStream)
@@ -17,4 +17,4 @@
*/ */
package com.rtbishop.look4sat.domain.predict package com.rtbishop.look4sat.domain.predict
data class GeoPos(val latitude: Double, val longitude: Double, val altitude: Double = 0.0) data class GeoPos(val latitude: Double, val longitude: Double, val name: String? = null)
@@ -168,10 +168,10 @@ abstract class Satellite(val params: TLE) {
val c = val c =
invert(sqrt(1.0 + flatFactor * (flatFactor - 2) * sqr(sin(deg2Rad * gsPos.latitude)))) invert(sqrt(1.0 + flatFactor * (flatFactor - 2) * sqr(sin(deg2Rad * gsPos.latitude))))
val sq = sqr(1.0 - flatFactor) * c val sq = sqr(1.0 - flatFactor) * c
val achcp = (earthRadius * c + gsPos.altitude / 1000.0) * cos(deg2Rad * gsPos.latitude) val achcp = (earthRadius * c) * cos(deg2Rad * gsPos.latitude)
obsPos.setXYZ( obsPos.setXYZ(
achcp * cos(gsPosTheta.get()), achcp * sin(gsPosTheta.get()), achcp * cos(gsPosTheta.get()), achcp * sin(gsPosTheta.get()),
(earthRadius * sq + gsPos.altitude / 1000.0) * sin(deg2Rad * gsPos.latitude) (earthRadius * sq) * sin(deg2Rad * gsPos.latitude)
) )
obsVel.setXYZ(-mFactor * obsPos.y, mFactor * obsPos.x, 0.0) obsVel.setXYZ(-mFactor * obsPos.y, mFactor * obsPos.x, 0.0)
magnitude(obsPos) magnitude(obsPos)