mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 19:17:22 +00:00
Now using repository external scope for sat data update
This commit is contained in:
1 parent
2427f8cbf5
commit
5b1bf943e9
10 files changed
+125
-121
No files matched your search
@@ -19,6 +19,6 @@ package com.rtbishop.look4sat.data
|
||||
|
||||
sealed class Result<out T : Any> {
|
||||
data class Success<out T : Any>(val data: T) : Result<T>()
|
||||
data class Error(val exception: Exception) : Result<Nothing>()
|
||||
data class Error(val exception: Throwable) : Result<Nothing>()
|
||||
object InProgress : Result<Nothing>()
|
||||
}
|
||||
@@ -27,26 +27,22 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ApplicationModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideContentResolver(@ApplicationContext context: Context): ContentResolver {
|
||||
return context.contentResolver
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLocationManager(@ApplicationContext context: Context): LocationManager {
|
||||
return context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
||||
@@ -26,29 +26,32 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CoroutinesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApplicationScope(): CoroutineScope = CoroutineScope(SupervisorJob())
|
||||
@ExternalScope
|
||||
fun provideExternalScope(): CoroutineScope = CoroutineScope(SupervisorJob())
|
||||
|
||||
@Provides
|
||||
@DefaultDispatcher
|
||||
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
|
||||
@Provides
|
||||
@IoDispatcher
|
||||
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
|
||||
@Provides
|
||||
@MainDispatcher
|
||||
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
}
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class ExternalScope
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class DefaultDispatcher
|
||||
|
||||
@@ -25,20 +25,17 @@ import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoshi(): Moshi {
|
||||
return Moshi.Builder().build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSatDataService(): SatelliteService {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl("https://db.satnogs.org/api/")
|
||||
|
||||
@@ -20,29 +20,26 @@ package com.rtbishop.look4sat.di
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.rtbishop.look4sat.repository.localData.MIGRATION_1_2
|
||||
import com.rtbishop.look4sat.repository.localData.RoomConverters
|
||||
import com.rtbishop.look4sat.repository.localData.SatelliteDao
|
||||
import com.rtbishop.look4sat.repository.localData.SatelliteDb
|
||||
import com.rtbishop.look4sat.repository.localData.RoomConverters
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object RepositoryModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSatDataDao(db: SatelliteDb): SatelliteDao {
|
||||
return db.satelliteDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSatelliteDb(@ApplicationContext context: Context, moshi: Moshi): SatelliteDb {
|
||||
RoomConverters.initialize(moshi)
|
||||
return Room.databaseBuilder(context, SatelliteDb::class.java, "satDb")
|
||||
|
||||
@@ -25,7 +25,9 @@ import com.rtbishop.look4sat.data.TleSource
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PrefsRepo @Inject constructor(val preferences: SharedPreferences, moshi: Moshi) {
|
||||
|
||||
private val sourcesType = Types.newParameterizedType(List::class.java, TleSource::class.java)
|
||||
|
||||
@@ -21,70 +21,102 @@ import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import com.github.amsacode.predict4java.Satellite
|
||||
import com.github.amsacode.predict4java.TLE
|
||||
import com.rtbishop.look4sat.data.SatEntry
|
||||
import com.rtbishop.look4sat.data.SatItem
|
||||
import com.rtbishop.look4sat.data.SatTrans
|
||||
import com.rtbishop.look4sat.data.TleSource
|
||||
import com.rtbishop.look4sat.data.*
|
||||
import com.rtbishop.look4sat.di.ExternalScope
|
||||
import com.rtbishop.look4sat.di.IoDispatcher
|
||||
import com.rtbishop.look4sat.repository.localData.SatelliteDao
|
||||
import com.rtbishop.look4sat.repository.remoteData.SatelliteService
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Singleton
|
||||
class SatelliteRepo @Inject constructor(
|
||||
private val resolver: ContentResolver,
|
||||
private val satelliteDao: SatelliteDao,
|
||||
private val satelliteService: SatelliteService,
|
||||
@ExternalScope private val externalScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
|
||||
fun getSatItems(): Flow<List<SatItem>> {
|
||||
return satelliteDao.getSatItems()
|
||||
private val _satData = MutableStateFlow<Result<List<SatItem>>>(Result.InProgress)
|
||||
val satData: Flow<Result<List<SatItem>>> = _satData
|
||||
|
||||
init {
|
||||
loadEntriesFromDb()
|
||||
}
|
||||
|
||||
suspend fun getSelectedSatellites(): List<Satellite> {
|
||||
return satelliteDao.getSelectedSatellites()
|
||||
}
|
||||
|
||||
suspend fun updateEntriesSelection(catNums: List<Int>) {
|
||||
satelliteDao.updateEntriesSelection(catNums)
|
||||
}
|
||||
|
||||
suspend fun updateEntriesFromFile(uri: Uri) {
|
||||
withContext(ioDispatcher) {
|
||||
runCatching {
|
||||
resolver.openInputStream(uri)?.use { stream ->
|
||||
updateAndRestoreSelection(importEntriesFromStreams(listOf(stream)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateEntriesFromWeb(sources: List<TleSource>) {
|
||||
withContext(ioDispatcher) {
|
||||
val streams = getStreamsForSources(sources)
|
||||
val entries = importEntriesFromStreams(streams)
|
||||
updateAndRestoreSelection(entries)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getTransmittersForSat(catNum: Int): Flow<List<SatTrans>> {
|
||||
return satelliteDao.getTransmittersForSat(catNum)
|
||||
}
|
||||
|
||||
suspend fun updateTransmitters() {
|
||||
satelliteDao.insertTransmitters(satelliteService.fetchTransmitters())
|
||||
|
||||
suspend fun getSelectedSatellites(): List<Satellite> {
|
||||
return satelliteDao.getSelectedSatellites()
|
||||
}
|
||||
|
||||
|
||||
suspend fun updateEntriesSelection(catNums: List<Int>) {
|
||||
satelliteDao.updateEntriesSelection(catNums)
|
||||
}
|
||||
|
||||
suspend fun updateSatDataFromFile(uri: Uri) {
|
||||
_satData.value = Result.InProgress
|
||||
externalScope.launch {
|
||||
runCatching {
|
||||
resolver.openInputStream(uri)?.use { stream ->
|
||||
val entries = importEntriesFromStreams(listOf(stream))
|
||||
insertEntriesAndRestoreSelection(entries)
|
||||
loadEntriesFromDb()
|
||||
}
|
||||
}.onFailure { throwable: Throwable ->
|
||||
Timber.d("$throwable")
|
||||
_satData.value = Result.Error(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateSatDataFromWeb(sources: List<TleSource>) {
|
||||
_satData.value = Result.InProgress
|
||||
externalScope.launch {
|
||||
val updateTimeMillis = measureTimeMillis {
|
||||
runCatching {
|
||||
val transmitters = satelliteService.fetchTransmitters()
|
||||
val streams = getStreamsForSources(sources)
|
||||
val entries = importEntriesFromStreams(streams)
|
||||
satelliteDao.insertTransmitters(transmitters)
|
||||
insertEntriesAndRestoreSelection(entries)
|
||||
loadEntriesFromDb()
|
||||
}.onFailure { throwable: Throwable ->
|
||||
Timber.d("$throwable")
|
||||
_satData.value = Result.Error(throwable)
|
||||
}
|
||||
}
|
||||
Timber.d("Update from Web took $updateTimeMillis ms")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadEntriesFromDb() {
|
||||
_satData.value = Result.InProgress
|
||||
externalScope.launch {
|
||||
satelliteDao.getSatItems().collect { satItems ->
|
||||
_satData.value = Result.Success(satItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getStreamsForSources(sources: List<TleSource>): List<InputStream> {
|
||||
val streams = mutableListOf<InputStream>()
|
||||
sources.forEach { tleSource ->
|
||||
satelliteService.fetchFile(tleSource.url).body()?.byteStream()?.let { inputStream ->
|
||||
if (tleSource.url.contains(".zip")) {
|
||||
if (tleSource.url.contains(".zip", true)) {
|
||||
// Handle zip stream
|
||||
val zipInputStream = ZipInputStream(inputStream)
|
||||
val zipEntry = zipInputStream.nextEntry
|
||||
@@ -96,17 +128,19 @@ class SatelliteRepo @Inject constructor(
|
||||
}
|
||||
return streams
|
||||
}
|
||||
|
||||
private fun importEntriesFromStreams(streams: List<InputStream>): List<SatEntry> {
|
||||
|
||||
private suspend fun importEntriesFromStreams(streams: List<InputStream>): List<SatEntry> {
|
||||
val importedEntries = mutableListOf<SatEntry>()
|
||||
streams.forEach { stream ->
|
||||
val entries = TLE.importSat(stream).map { tle -> SatEntry(tle) }
|
||||
importedEntries.addAll(entries)
|
||||
withContext(ioDispatcher) {
|
||||
streams.forEach { stream ->
|
||||
val entries = TLE.importSat(stream).map { tle -> SatEntry(tle) }
|
||||
importedEntries.addAll(entries)
|
||||
}
|
||||
}
|
||||
return importedEntries
|
||||
}
|
||||
|
||||
private suspend fun updateAndRestoreSelection(entries: List<SatEntry>) {
|
||||
|
||||
private suspend fun insertEntriesAndRestoreSelection(entries: List<SatEntry>) {
|
||||
val selectedCatNums = satelliteDao.getSelectedCatNums()
|
||||
satelliteDao.insertEntries(entries)
|
||||
satelliteDao.updateEntriesSelection(selectedCatNums)
|
||||
|
||||
@@ -26,37 +26,37 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SatelliteDao {
|
||||
|
||||
|
||||
@Query("SELECT * FROM entries ORDER BY name ASC")
|
||||
fun getEntries(): Flow<List<SatEntry>>
|
||||
|
||||
|
||||
@Query("SELECT catNum, name, isSelected FROM entries ORDER BY name ASC")
|
||||
fun getSatItems(): Flow<List<SatItem>>
|
||||
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertEntries(entries: List<SatEntry>)
|
||||
|
||||
|
||||
@Query("SELECT tle FROM entries WHERE isSelected = 1")
|
||||
suspend fun getSelectedSatellites(): List<Satellite>
|
||||
|
||||
|
||||
@Query("SELECT catNum FROM entries WHERE isSelected = 1")
|
||||
suspend fun getSelectedCatNums(): List<Int>
|
||||
|
||||
|
||||
@Transaction
|
||||
suspend fun updateEntriesSelection(catNums: List<Int>) {
|
||||
clearSelection()
|
||||
catNums.forEach { catNum -> updateSelection(catNum) }
|
||||
}
|
||||
|
||||
|
||||
@Query("UPDATE entries SET isSelected = 0")
|
||||
suspend fun clearSelection()
|
||||
|
||||
|
||||
@Query("UPDATE entries SET isSelected = 1 WHERE catNum = :catNum")
|
||||
suspend fun updateSelection(catNum: Int)
|
||||
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertTransmitters(transmitters: List<SatTrans>)
|
||||
|
||||
|
||||
@Query("SELECT * FROM transmitters WHERE isAlive = 1 and catNum = :catNum")
|
||||
fun getTransmittersForSat(catNum: Int): Flow<List<SatTrans>>
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
@@ -41,32 +40,30 @@ class SharedViewModel @Inject constructor(
|
||||
private val prefsRepo: PrefsRepo,
|
||||
private val satelliteRepo: SatelliteRepo,
|
||||
) : ViewModel() {
|
||||
|
||||
|
||||
private val _passes = MutableStateFlow<Result<MutableList<SatPass>>>(Result.InProgress)
|
||||
val passes: LiveData<Result<MutableList<SatPass>>> = _passes.asLiveData()
|
||||
|
||||
private val _satData = MutableStateFlow<Result<List<SatItem>>>(Result.InProgress)
|
||||
val satData: LiveData<Result<List<SatItem>>> = _satData.asLiveData()
|
||||
|
||||
|
||||
val satData = satelliteRepo.satData.asLiveData()
|
||||
|
||||
init {
|
||||
if (prefsRepo.isFirstLaunch()) {
|
||||
updateDefaultSourcesAndEntries()
|
||||
prefsRepo.setFirstLaunchDone()
|
||||
}
|
||||
loadDataFromDb()
|
||||
calculatePasses()
|
||||
}
|
||||
|
||||
|
||||
fun getAppTimer() = liveData {
|
||||
while (true) {
|
||||
emit(System.currentTimeMillis())
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getSources() = prefsRepo.loadTleSources()
|
||||
fun getTransmittersForSat(satId: Int) = satelliteRepo.getTransmittersForSat(satId).asLiveData()
|
||||
|
||||
|
||||
fun calculatePasses(dateNow: Date = Date(System.currentTimeMillis())) {
|
||||
_passes.value = Result.InProgress
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
@@ -78,31 +75,20 @@ class SharedViewModel @Inject constructor(
|
||||
_passes.value = Result.Success(filteredPasses)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateEntriesFromFile(uri: Uri) {
|
||||
_satData.value = Result.InProgress
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
satelliteRepo.updateEntriesFromFile(uri)
|
||||
} catch (exception: Exception) {
|
||||
_satData.value = Result.Error(exception)
|
||||
}
|
||||
satelliteRepo.updateSatDataFromFile(uri)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateEntriesFromSources(sources: List<TleSource> = prefsRepo.loadTleSources()) {
|
||||
_satData.value = Result.InProgress
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
prefsRepo.saveTleSources(sources)
|
||||
satelliteRepo.updateEntriesFromWeb(sources)
|
||||
satelliteRepo.updateTransmitters()
|
||||
} catch (exception: Exception) {
|
||||
_satData.value = Result.Error(exception)
|
||||
}
|
||||
prefsRepo.saveTleSources(sources)
|
||||
satelliteRepo.updateSatDataFromWeb(sources)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateItemsSelection(items: List<SatItem>) {
|
||||
_passes.value = Result.InProgress
|
||||
viewModelScope.launch {
|
||||
@@ -111,14 +97,14 @@ class SharedViewModel @Inject constructor(
|
||||
calculatePasses()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getPasses(satellite: Satellite, dateNow: Date): MutableList<SatPass> {
|
||||
val predictor = satellite.getPredictor(prefsRepo.getStationPosition())
|
||||
val passes = predictor.getPasses(dateNow, prefsRepo.getHoursAhead(), true)
|
||||
val passList = passes.map { SatPass(satellite.tle, predictor, it) }
|
||||
return passList as MutableList<SatPass>
|
||||
}
|
||||
|
||||
|
||||
private fun sortList(passes: MutableList<SatPass>, dateNow: Date): MutableList<SatPass> {
|
||||
val hoursAhead = prefsRepo.getHoursAhead()
|
||||
val dateFuture = Calendar.getInstance().apply {
|
||||
@@ -131,18 +117,7 @@ class SharedViewModel @Inject constructor(
|
||||
passes.sortBy { it.pass.startTime }
|
||||
return passes
|
||||
}
|
||||
|
||||
private fun loadDataFromDb() {
|
||||
_satData.value = Result.InProgress
|
||||
viewModelScope.launch {
|
||||
satelliteRepo.getSatItems().collect { satItems: List<SatItem> ->
|
||||
if (satItems.isNotEmpty()) {
|
||||
_satData.value = Result.Success(satItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateDefaultSourcesAndEntries() {
|
||||
updateEntriesFromSources()
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EntriesFragment : Fragment(R.layout.fragment_entries) {
|
||||
|
||||
|
||||
private val viewModel: SharedViewModel by activityViewModels()
|
||||
private val filePicker =
|
||||
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
@@ -44,14 +44,14 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) {
|
||||
}
|
||||
private var binding: FragmentEntriesBinding? = null
|
||||
private var entriesAdapter: EntriesAdapter? = null
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentEntriesBinding.bind(view)
|
||||
setupComponents()
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
|
||||
private fun setupComponents() {
|
||||
entriesAdapter = EntriesAdapter()
|
||||
binding?.apply {
|
||||
@@ -69,7 +69,7 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) {
|
||||
searchBar.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.satData.observe(viewLifecycleOwner, { result ->
|
||||
when (result) {
|
||||
@@ -110,7 +110,7 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) {
|
||||
entriesProgress.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setError() {
|
||||
binding?.apply {
|
||||
entriesProgress.visibility = View.INVISIBLE
|
||||
@@ -118,11 +118,11 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) {
|
||||
entriesError.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun showImportFromWebDialog() {
|
||||
findNavController().navigate(R.id.nav_dialog_sources)
|
||||
}
|
||||
|
||||
|
||||
private fun navigateToPasses() {
|
||||
binding?.searchBar?.clearFocus()
|
||||
entriesAdapter?.let {
|
||||
|
||||
Reference in new issue
Block a user