Now using repository external scope for sat data update

This commit is contained in:
Arty Bishop committed 2021-03-14 20:33:28 +00:00
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 {