kopia lustrzana https://github.com/rt-bishop/Look4Sat
Moved sources urls to database storage
rodzic
af7e027350
commit
5f4c63ee49
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "825f728737780384e51a6a73221288c4",
|
||||
"identityHash": "555494c464b5a29285eb5493cc8aa5f6",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "entries",
|
||||
|
@ -103,12 +103,32 @@
|
|||
},
|
||||
"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, '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.SensorManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.round
|
||||
|
||||
@Singleton
|
||||
class OrientationSource @Inject constructor(private val sensorManager: SensorManager) :
|
||||
SensorEventListener {
|
||||
|
||||
|
|
|
@ -21,50 +21,19 @@ import android.content.SharedPreferences
|
|||
import android.hardware.GeomagneticField
|
||||
import android.location.LocationManager
|
||||
import androidx.core.content.edit
|
||||
import com.rtbishop.look4sat.domain.Constants
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.domain.QthConverter
|
||||
import com.rtbishop.look4sat.utility.round
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PreferencesSource @Inject constructor(
|
||||
moshi: Moshi,
|
||||
private val locationManager: LocationManager,
|
||||
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 {
|
||||
const val keySources = "prefTleSourcesKey"
|
||||
const val keyModes = "satModes"
|
||||
const val keyCompass = "compass"
|
||||
const val keyRadarSweep = "radarSweep"
|
||||
|
@ -78,7 +47,6 @@ class PreferencesSource @Inject constructor(
|
|||
const val keyRotatorPort = "rotatorPort"
|
||||
const val keyLatitude = "stationLat"
|
||||
const val keyLongitude = "stationLon"
|
||||
const val keyAltitude = "stationAlt"
|
||||
const val keyPositionGPS = "setPositionGPS"
|
||||
const val keyPositionQTH = "setPositionQTH"
|
||||
}
|
||||
|
@ -87,15 +55,13 @@ class PreferencesSource @Inject constructor(
|
|||
val defaultSP = "0.0"
|
||||
val latitude = preferences.getString(keyLatitude, null) ?: defaultSP
|
||||
val longitude = preferences.getString(keyLongitude, null) ?: defaultSP
|
||||
val altitude = preferences.getString(keyAltitude, null) ?: defaultSP
|
||||
return GeoPos(latitude.toDouble(), longitude.toDouble(), altitude.toDouble())
|
||||
return GeoPos(latitude.toDouble(), longitude.toDouble())
|
||||
}
|
||||
|
||||
fun saveStationPosition(pos: GeoPos) {
|
||||
preferences.edit {
|
||||
putString(keyLatitude, pos.latitude.toString())
|
||||
putString(keyLongitude, pos.longitude.toString())
|
||||
putString(keyAltitude, pos.altitude.toString())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,8 +72,7 @@ class PreferencesSource @Inject constructor(
|
|||
else {
|
||||
val latitude = location.latitude.round(4)
|
||||
val longitude = location.longitude.round(4)
|
||||
val altitude = location.altitude.round(1)
|
||||
val stationPosition = GeoPos(latitude, longitude, altitude)
|
||||
val stationPosition = GeoPos(latitude, longitude)
|
||||
saveStationPosition(stationPosition)
|
||||
return true
|
||||
}
|
||||
|
@ -118,7 +83,7 @@ class PreferencesSource @Inject constructor(
|
|||
|
||||
fun updatePositionFromQTH(qthString: String): Boolean {
|
||||
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)
|
||||
return true
|
||||
}
|
||||
|
@ -127,8 +92,7 @@ class PreferencesSource @Inject constructor(
|
|||
val stationPosition = loadStationPosition()
|
||||
val lat = stationPosition.latitude.toFloat()
|
||||
val lon = stationPosition.longitude.toFloat()
|
||||
val alt = stationPosition.altitude.toFloat()
|
||||
return GeomagneticField(lat, lon, alt, System.currentTimeMillis()).declination
|
||||
return GeomagneticField(lat, lon, 0f, System.currentTimeMillis()).declination
|
||||
}
|
||||
|
||||
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.model.Transmitter
|
||||
import com.rtbishop.look4sat.framework.DataMapper
|
||||
import com.rtbishop.look4sat.framework.model.DataSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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>> {
|
||||
return satelliteDao.getSatItems()
|
||||
.map { satItems -> DataMapper.satItemsToDomainItems(satItems) }
|
||||
}
|
||||
|
||||
override suspend fun getSources(): List<String> {
|
||||
return sourcesDao.getSources()
|
||||
}
|
||||
|
||||
override suspend fun getSelectedSatellites(): List<Satellite> {
|
||||
return satelliteDao.getSelectedSatellites()
|
||||
}
|
||||
|
@ -46,6 +54,11 @@ class LocalSource(private val satelliteDao: SatelliteDao) : LocalDataSource {
|
|||
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>> {
|
||||
return satelliteDao.getSatTransmitters(catNum)
|
||||
.map { satTransList -> DataMapper.satTransListToDomainTransList(satTransList) }
|
||||
|
|
|
@ -22,14 +22,21 @@ import androidx.room.RoomDatabase
|
|||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
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.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)
|
||||
abstract class SatelliteDb : RoomDatabase() {
|
||||
|
||||
abstract fun satelliteDao(): SatelliteDao
|
||||
|
||||
abstract fun sourcesDao(): SourcesDao
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
* 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 androidx.preference.PreferenceManager
|
||||
import androidx.room.Room
|
||||
import com.rtbishop.look4sat.domain.Constants
|
||||
import com.rtbishop.look4sat.framework.PreferencesSource
|
||||
import com.rtbishop.look4sat.framework.local.Converters
|
||||
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.local.*
|
||||
import com.rtbishop.look4sat.framework.remote.SatelliteApi
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.Module
|
||||
|
@ -74,18 +70,17 @@ object AppModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun providePreferenceSource(
|
||||
moshi: Moshi,
|
||||
locationManager: LocationManager,
|
||||
preferences: SharedPreferences
|
||||
): PreferencesSource {
|
||||
return PreferencesSource(moshi, locationManager, preferences)
|
||||
return PreferencesSource(locationManager, preferences)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSatelliteApi(): SatelliteApi {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(Constants.URL_BASE)
|
||||
.baseUrl("https://db.satnogs.org/api/")
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.build().create(SatelliteApi::class.java)
|
||||
}
|
||||
|
@ -96,11 +91,17 @@ object AppModule {
|
|||
return db.satelliteDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSourcesDao(db: SatelliteDb): SourcesDao {
|
||||
return db.sourcesDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSatelliteDb(@ApplicationContext context: Context, moshi: Moshi): SatelliteDb {
|
||||
Converters.initialize(moshi)
|
||||
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 {
|
||||
|
||||
@Provides
|
||||
fun provideLocalDataSource(satelliteDao: SatelliteDao): LocalDataSource {
|
||||
return LocalSource(satelliteDao)
|
||||
fun provideLocalDataSource(
|
||||
satelliteDao: SatelliteDao,
|
||||
sourcesDao: SourcesDao
|
||||
): LocalDataSource {
|
||||
return LocalSource(satelliteDao, sourcesDao)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
@ -37,7 +37,7 @@ import javax.inject.Inject
|
|||
class EntriesViewModel @Inject constructor(
|
||||
private val preferences: PreferencesSource,
|
||||
private val resolver: ContentResolver,
|
||||
private val satelliteRepo: SatelliteRepo,
|
||||
private val satelliteRepo: SatelliteRepo
|
||||
) : ViewModel(), SearchView.OnQueryTextListener {
|
||||
|
||||
private val coroutineHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
|
@ -72,7 +72,6 @@ class EntriesViewModel @Inject constructor(
|
|||
fun updateEntriesFromWeb(sources: List<String>) {
|
||||
viewModelScope.launch(coroutineHandler) {
|
||||
_satData.value = DataState.Loading
|
||||
preferences.saveTleSources(sources)
|
||||
satelliteRepo.updateEntriesFromWeb(sources)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,9 +41,14 @@ import java.util.*
|
|||
class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesClickListener {
|
||||
|
||||
private val passesViewModel: PassesViewModel by viewModels()
|
||||
private val permRequest = ActivityResultContracts.RequestPermission()
|
||||
private val permRequestLauncher = registerForActivityResult(permRequest) {
|
||||
passesViewModel.triggerInitialSetup()
|
||||
private val permReqContract = ActivityResultContracts.RequestMultiplePermissions()
|
||||
private val locPermFine = Manifest.permission.ACCESS_FINE_LOCATION
|
||||
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?) {
|
||||
|
@ -68,7 +73,7 @@ class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesC
|
|||
handleNewPasses(passesResult, passesAdapter, binding)
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
is DataState.Error -> {
|
||||
else -> {
|
||||
binding.apply {
|
||||
passesTimer.text = 0L.toTimerString()
|
||||
passesProgress.visibility = View.INVISIBLE
|
||||
|
|
|
@ -77,7 +77,7 @@ class PassesViewModel @Inject constructor(
|
|||
val stationPos = preferences.loadStationPosition()
|
||||
val hoursAhead = preferences.getHoursAhead()
|
||||
val minElev = preferences.getMinElevation()
|
||||
satelliteRepo.updateEntriesFromWeb(preferences.loadDefaultSources())
|
||||
satelliteRepo.updateEntriesFromWeb(satelliteRepo.getDefaultSources())
|
||||
satelliteRepo.updateEntriesSelection(defaultCatNums, true)
|
||||
predictor.forceCalculation(satellites, stationPos, dateNow, hoursAhead, minElev)
|
||||
preferences.setSetupDone()
|
||||
|
|
|
@ -39,14 +39,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
@Inject
|
||||
lateinit var preferences: PreferencesSource
|
||||
|
||||
private val requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
updatePositionFromGPS()
|
||||
} else {
|
||||
showSnack(getString(R.string.pref_pos_gps_error))
|
||||
}
|
||||
private val permReqContract = ActivityResultContracts.RequestMultiplePermissions()
|
||||
private val locPermFine = Manifest.permission.ACCESS_FINE_LOCATION
|
||||
private val locPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
private val locPermReq = registerForActivityResult(permReqContract) { permissions ->
|
||||
when {
|
||||
permissions[locPermFine] == true -> updatePositionFromGPS()
|
||||
permissions[locPermCoarse] == true -> updatePositionFromGPS()
|
||||
else -> showSnack(getString(R.string.pref_pos_gps_error))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preference, rootKey)
|
||||
|
@ -113,7 +115,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
} else {
|
||||
showSnack(getString(R.string.pref_pos_gps_null))
|
||||
}
|
||||
} else requestPermissionLauncher.launch(locPermString)
|
||||
} else locPermReq.launch(arrayOf(locPermFine, locPermCoarse))
|
||||
}
|
||||
|
||||
private fun showSnack(message: String) {
|
||||
|
|
|
@ -22,12 +22,13 @@ import android.view.ViewGroup
|
|||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.rtbishop.look4sat.databinding.ItemSourceBinding
|
||||
import com.rtbishop.look4sat.framework.model.DataSource
|
||||
|
||||
class SourcesAdapter(private val sources: MutableList<DataSource> = mutableListOf()) :
|
||||
RecyclerView.Adapter<SourcesAdapter.TleSourceHolder>() {
|
||||
|
||||
fun getSources(): List<DataSource> {
|
||||
return sources.filter { it.url.contains("https://") }
|
||||
return sources.filter { it.sourceUrl.contains("https://") }
|
||||
}
|
||||
|
||||
fun setSources(list: List<DataSource>) {
|
||||
|
@ -58,8 +59,8 @@ class SourcesAdapter(private val sources: MutableList<DataSource> = mutableListO
|
|||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(source: DataSource) {
|
||||
binding.sourceUrl.setText(source.url)
|
||||
binding.sourceUrl.doOnTextChanged { text, _, _, _ -> source.url = text.toString() }
|
||||
binding.sourceUrl.setText(source.sourceUrl)
|
||||
binding.sourceUrl.doOnTextChanged { text, _, _, _ -> source.sourceUrl = text.toString() }
|
||||
binding.sourceInput.setEndIconOnClickListener {
|
||||
sources.remove(source)
|
||||
notifyItemRemoved(adapterPosition)
|
||||
|
|
|
@ -23,19 +23,18 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.rtbishop.look4sat.R
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SourcesDialog : AppCompatDialogFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var prefsManager: PreferencesSource
|
||||
private val viewModel: SourcesViewModel by viewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.dialog_sources, group, false)
|
||||
|
@ -43,25 +42,26 @@ class SourcesDialog : AppCompatDialogFragment() {
|
|||
|
||||
override fun onViewCreated(view: View, state: Bundle?) {
|
||||
super.onViewCreated(view, state)
|
||||
val sources = prefsManager.loadTleSources().map { DataSource(it) }
|
||||
val sourcesAdapter = SourcesAdapter().apply { setSources(sources) }
|
||||
DialogSourcesBinding.bind(view).apply {
|
||||
dialog?.window?.setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
sourcesRecycler.apply {
|
||||
adapter = sourcesAdapter
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
viewModel.sources.observe(viewLifecycleOwner, { sources ->
|
||||
val adapter = SourcesAdapter().apply { setSources(sources.map { DataSource(it) }) }
|
||||
DialogSourcesBinding.bind(view).apply {
|
||||
dialog?.window?.setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
sourcesRecycler.apply {
|
||||
this.adapter = adapter
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
}
|
||||
sourcesBtnAdd.setOnClickListener {
|
||||
adapter.addSource()
|
||||
}
|
||||
sourcesBtnPos.setOnClickListener {
|
||||
setNavResult("sources", adapter.getSources().map { it.sourceUrl })
|
||||
dismiss()
|
||||
}
|
||||
sourcesBtnNeg.setOnClickListener { dismiss() }
|
||||
}
|
||||
sourcesBtnAdd.setOnClickListener {
|
||||
sourcesAdapter.addSource()
|
||||
}
|
||||
sourcesBtnPos.setOnClickListener {
|
||||
setNavResult("sources", sourcesAdapter.getSources().map { it.url })
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
buildscript {
|
||||
ext {
|
||||
gradle_version = '7.0.2'
|
||||
gradle_version = '7.0.3'
|
||||
kotlin_version = '1.5.31'
|
||||
coroutines_version = '1.5.2-native-mt'
|
||||
material_version = '1.4.0'
|
||||
|
@ -15,7 +15,7 @@ buildscript {
|
|||
osmdroid_version = '6.1.11'
|
||||
timber_version = '5.0.1'
|
||||
junit_version = '4.13.2'
|
||||
mockito_version = '3.12.4'
|
||||
mockito_version = '4.0.0'
|
||||
leak_canary_version = '2.7'
|
||||
}
|
||||
repositories {
|
||||
|
|
|
@ -45,6 +45,22 @@ class DataRepository(
|
|||
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> {
|
||||
return localSource.getSelectedSatellites()
|
||||
}
|
||||
|
@ -55,6 +71,9 @@ class DataRepository(
|
|||
|
||||
override suspend fun updateEntriesFromWeb(sources: List<String>) {
|
||||
coroutineScope {
|
||||
launch(repoDispatcher) {
|
||||
localSource.updateSources(sources)
|
||||
}
|
||||
launch(repoDispatcher) {
|
||||
val streams = mutableListOf<InputStream>()
|
||||
val entries = mutableListOf<SatEntry>()
|
||||
|
|
|
@ -29,11 +29,15 @@ interface LocalDataSource {
|
|||
|
||||
fun getTransmitters(catNum: Int): Flow<List<Transmitter>>
|
||||
|
||||
suspend fun getSources(): List<String>
|
||||
|
||||
suspend fun getSelectedSatellites(): List<Satellite>
|
||||
|
||||
suspend fun updateEntries(entries: List<SatEntry>)
|
||||
|
||||
suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean)
|
||||
|
||||
suspend fun updateSources(sources: List<String>)
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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 {
|
||||
val qthPattern = "[a-xA-X][a-xA-X][0-9][0-9][a-xA-X][a-xA-X]".toRegex()
|
||||
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 {
|
||||
var multiplier = 1.0
|
||||
repeat(decimals) { multiplier *= 10 }
|
||||
|
|
|
@ -29,6 +29,10 @@ interface SatelliteRepo {
|
|||
|
||||
fun getSatTransmitters(catNum: Int): Flow<List<Transmitter>>
|
||||
|
||||
fun getDefaultSources(): List<String>
|
||||
|
||||
suspend fun getSavedSources(): List<String>
|
||||
|
||||
suspend fun getSelectedSatellites(): List<Satellite>
|
||||
|
||||
suspend fun updateEntriesFromFile(stream: InputStream)
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
*/
|
||||
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 =
|
||||
invert(sqrt(1.0 + flatFactor * (flatFactor - 2) * sqr(sin(deg2Rad * gsPos.latitude))))
|
||||
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(
|
||||
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)
|
||||
magnitude(obsPos)
|
||||
|
|
Ładowanie…
Reference in New Issue