diff --git a/app/schemas/com.rtbishop.look4sat.framework.local.SatelliteDb/2.json b/app/schemas/com.rtbishop.look4sat.framework.local.SatelliteDb/2.json index 5c523604..f23c6ef8 100644 --- a/app/schemas/com.rtbishop.look4sat.framework.local.SatelliteDb/2.json +++ b/app/schemas/com.rtbishop.look4sat.framework.local.SatelliteDb/2.json @@ -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')" ] } } \ No newline at end of file diff --git a/app/schemas/com.rtbishop.look4sat.framework.local.SatelliteDb/3.json b/app/schemas/com.rtbishop.look4sat.framework.local.SatelliteDb/3.json new file mode 100644 index 00000000..1ffc1e01 --- /dev/null +++ b/app/schemas/com.rtbishop.look4sat.framework.local.SatelliteDb/3.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rtbishop/look4sat/framework/LocationSource.kt b/app/src/main/java/com/rtbishop/look4sat/framework/LocationSource.kt new file mode 100644 index 00000000..19f0b82a --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/framework/LocationSource.kt @@ -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() + override val updatedLocation: SharedFlow = _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)) + } +} diff --git a/app/src/main/java/com/rtbishop/look4sat/framework/OrientationSource.kt b/app/src/main/java/com/rtbishop/look4sat/framework/OrientationSource.kt index 896665a6..b168724f 100644 --- a/app/src/main/java/com/rtbishop/look4sat/framework/OrientationSource.kt +++ b/app/src/main/java/com/rtbishop/look4sat/framework/OrientationSource.kt @@ -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 { diff --git a/app/src/main/java/com/rtbishop/look4sat/framework/PreferencesSource.kt b/app/src/main/java/com/rtbishop/look4sat/framework/PreferencesSource.kt index fb206b98..ba6ebaca 100644 --- a/app/src/main/java/com/rtbishop/look4sat/framework/PreferencesSource.kt +++ b/app/src/main/java/com/rtbishop/look4sat/framework/PreferencesSource.kt @@ -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>(sourcesType) - - fun loadTleSources(): List { - 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) { - val sourcesJson = sourcesAdapter.toJson(sources) - preferences.edit { putString(keySources, sourcesJson) } - } - - fun loadDefaultSources(): List { - 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 { diff --git a/app/src/main/java/com/rtbishop/look4sat/framework/local/LocalSource.kt b/app/src/main/java/com/rtbishop/look4sat/framework/local/LocalSource.kt index 54398062..f6aaea14 100644 --- a/app/src/main/java/com/rtbishop/look4sat/framework/local/LocalSource.kt +++ b/app/src/main/java/com/rtbishop/look4sat/framework/local/LocalSource.kt @@ -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> { return satelliteDao.getSatItems() .map { satItems -> DataMapper.satItemsToDomainItems(satItems) } } + override suspend fun getSources(): List { + return sourcesDao.getSources() + } + override suspend fun getSelectedSatellites(): List { return satelliteDao.getSelectedSatellites() } @@ -46,6 +54,11 @@ class LocalSource(private val satelliteDao: SatelliteDao) : LocalDataSource { satelliteDao.updateEntriesSelection(catNums, isSelected) } + override suspend fun updateSources(sources: List) { + sourcesDao.deleteSources() + sourcesDao.setSources(sources.map { DataSource(it) }) + } + override fun getTransmitters(catNum: Int): Flow> { return satelliteDao.getSatTransmitters(catNum) .map { satTransList -> DataMapper.satTransListToDomainTransList(satTransList) } diff --git a/app/src/main/java/com/rtbishop/look4sat/framework/local/SatelliteDb.kt b/app/src/main/java/com/rtbishop/look4sat/framework/local/SatelliteDb.kt index bc08e8bd..4f867f17 100644 --- a/app/src/main/java/com/rtbishop/look4sat/framework/local/SatelliteDb.kt +++ b/app/src/main/java/com/rtbishop/look4sat/framework/local/SatelliteDb.kt @@ -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))") + } +} diff --git a/app/src/main/java/com/rtbishop/look4sat/framework/local/SourcesDao.kt b/app/src/main/java/com/rtbishop/look4sat/framework/local/SourcesDao.kt new file mode 100644 index 00000000..66a53efd --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/framework/local/SourcesDao.kt @@ -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 + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun setSources(sources: List) + + @Query("DELETE from sources") + suspend fun deleteSources() +} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/DataSource.kt b/app/src/main/java/com/rtbishop/look4sat/framework/model/DataSource.kt similarity index 79% rename from app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/DataSource.kt rename to app/src/main/java/com/rtbishop/look4sat/framework/model/DataSource.kt index d00ee7bd..3a8b833a 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/DataSource.kt +++ b/app/src/main/java/com/rtbishop/look4sat/framework/model/DataSource.kt @@ -15,6 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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()) diff --git a/app/src/main/java/com/rtbishop/look4sat/injection/AppModule.kt b/app/src/main/java/com/rtbishop/look4sat/injection/AppModule.kt index f4dc8ab4..6bd9fa2e 100644 --- a/app/src/main/java/com/rtbishop/look4sat/injection/AppModule.kt +++ b/app/src/main/java/com/rtbishop/look4sat/injection/AppModule.kt @@ -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() } } diff --git a/app/src/main/java/com/rtbishop/look4sat/injection/CoreModule.kt b/app/src/main/java/com/rtbishop/look4sat/injection/CoreModule.kt index 9d4c6a91..5bcf0aa7 100644 --- a/app/src/main/java/com/rtbishop/look4sat/injection/CoreModule.kt +++ b/app/src/main/java/com/rtbishop/look4sat/injection/CoreModule.kt @@ -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 diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesViewModel.kt index 01fef101..6c2c294e 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesViewModel.kt @@ -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) { viewModelScope.launch(coroutineHandler) { _satData.value = DataState.Loading - preferences.saveTleSources(sources) satelliteRepo.updateEntriesFromWeb(sources) } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesFragment.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesFragment.kt index 56bf716a..b0e3e338 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesFragment.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesFragment.kt @@ -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 diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt index e13b2a48..eb2a7163 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt @@ -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() diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsFragment.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsFragment.kt index 1634469b..7422db94 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsFragment.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsFragment.kt @@ -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) { diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesAdapter.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesAdapter.kt index bf96b3b0..2896b8bd 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesAdapter.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesAdapter.kt @@ -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 = mutableListOf()) : RecyclerView.Adapter() { fun getSources(): List { - return sources.filter { it.url.contains("https://") } + return sources.filter { it.sourceUrl.contains("https://") } } fun setSources(list: List) { @@ -58,8 +59,8 @@ class SourcesAdapter(private val sources: MutableList = 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) diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesDialog.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesDialog.kt index 7f7dede8..8cee6e14 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesDialog.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesDialog.kt @@ -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() } - } + }) } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesViewModel.kt new file mode 100644 index 00000000..eadbdc6f --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/sourcesScreen/SourcesViewModel.kt @@ -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()) + } +} diff --git a/build.gradle b/build.gradle index eca85953..1128f877 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/core/src/main/java/com/rtbishop/look4sat/data/DataRepository.kt b/core/src/main/java/com/rtbishop/look4sat/data/DataRepository.kt index dc1f10a7..0e0741e2 100644 --- a/core/src/main/java/com/rtbishop/look4sat/data/DataRepository.kt +++ b/core/src/main/java/com/rtbishop/look4sat/data/DataRepository.kt @@ -45,6 +45,22 @@ class DataRepository( return localSource.getTransmitters(catNum) } + override fun getDefaultSources(): List { + 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 { + val savedSources = localSource.getSources() + return if (savedSources.isEmpty()) { + getDefaultSources() + } else savedSources + } + override suspend fun getSelectedSatellites(): List { return localSource.getSelectedSatellites() } @@ -55,6 +71,9 @@ class DataRepository( override suspend fun updateEntriesFromWeb(sources: List) { coroutineScope { + launch(repoDispatcher) { + localSource.updateSources(sources) + } launch(repoDispatcher) { val streams = mutableListOf() val entries = mutableListOf() diff --git a/core/src/main/java/com/rtbishop/look4sat/data/LocalDataSource.kt b/core/src/main/java/com/rtbishop/look4sat/data/LocalDataSource.kt index fc77cde1..e75f0b9e 100644 --- a/core/src/main/java/com/rtbishop/look4sat/data/LocalDataSource.kt +++ b/core/src/main/java/com/rtbishop/look4sat/data/LocalDataSource.kt @@ -29,11 +29,15 @@ interface LocalDataSource { fun getTransmitters(catNum: Int): Flow> + suspend fun getSources(): List + suspend fun getSelectedSatellites(): List suspend fun updateEntries(entries: List) suspend fun updateEntriesSelection(catNums: List, isSelected: Boolean) + suspend fun updateSources(sources: List) + suspend fun updateTransmitters(transmitters: List) } diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/Constants.kt b/core/src/main/java/com/rtbishop/look4sat/domain/Constants.kt deleted file mode 100644 index e94fef76..00000000 --- a/core/src/main/java/com/rtbishop/look4sat/domain/Constants.kt +++ /dev/null @@ -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 . - */ -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" -} diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/LocationProvider.kt b/core/src/main/java/com/rtbishop/look4sat/domain/LocationProvider.kt new file mode 100644 index 00000000..8ec48454 --- /dev/null +++ b/core/src/main/java/com/rtbishop/look4sat/domain/LocationProvider.kt @@ -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 +} diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/QthConverter.kt b/core/src/main/java/com/rtbishop/look4sat/domain/QthConverter.kt index cbde6448..126b492f 100644 --- a/core/src/main/java/com/rtbishop/look4sat/domain/QthConverter.kt +++ b/core/src/main/java/com/rtbishop/look4sat/domain/QthConverter.kt @@ -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 } diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/SatelliteRepo.kt b/core/src/main/java/com/rtbishop/look4sat/domain/SatelliteRepo.kt index 0a30b751..ac84db90 100644 --- a/core/src/main/java/com/rtbishop/look4sat/domain/SatelliteRepo.kt +++ b/core/src/main/java/com/rtbishop/look4sat/domain/SatelliteRepo.kt @@ -29,6 +29,10 @@ interface SatelliteRepo { fun getSatTransmitters(catNum: Int): Flow> + fun getDefaultSources(): List + + suspend fun getSavedSources(): List + suspend fun getSelectedSatellites(): List suspend fun updateEntriesFromFile(stream: InputStream) diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/predict/GeoPos.kt b/core/src/main/java/com/rtbishop/look4sat/domain/predict/GeoPos.kt index 2f26f112..6f02663e 100644 --- a/core/src/main/java/com/rtbishop/look4sat/domain/predict/GeoPos.kt +++ b/core/src/main/java/com/rtbishop/look4sat/domain/predict/GeoPos.kt @@ -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) diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/predict/Satellite.kt b/core/src/main/java/com/rtbishop/look4sat/domain/predict/Satellite.kt index 7c12519f..4d11a8b6 100644 --- a/core/src/main/java/com/rtbishop/look4sat/domain/predict/Satellite.kt +++ b/core/src/main/java/com/rtbishop/look4sat/domain/predict/Satellite.kt @@ -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)