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 205c7b18..5091f6f9 100644 --- a/app/src/main/java/com/rtbishop/look4sat/injection/CoreModule.kt +++ b/app/src/main/java/com/rtbishop/look4sat/injection/CoreModule.kt @@ -37,6 +37,8 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -46,7 +48,7 @@ object CoreModule { @Provides @Singleton fun provideSatelliteRepo( - settingsHandler: ISettingsHandler, + settings: ISettingsHandler, @ApplicationContext context: Context, @IoDispatcher ioDispatcher: CoroutineDispatcher, @DefaultDispatcher defaultDispatcher: CoroutineDispatcher @@ -57,7 +59,8 @@ object CoreModule { .fallbackToDestructiveMigration().build() val localSource = LocalSource(db.entriesDao(), db.transmittersDao()) val remoteSource = RemoteSource(ioDispatcher) - return DataRepository(dataParser, localSource, remoteSource, settingsHandler) + val repositoryScope = CoroutineScope(SupervisorJob()) + return DataRepository(dataParser, settings, localSource, remoteSource, repositoryScope) } @Provides 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 6783dcc3..c14a0e5d 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 @@ -56,7 +56,7 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) { } private val contentContract = ActivityResultContracts.GetContent() private val contentRequest = registerForActivityResult(contentContract) { uri -> - viewModel.updateDataFromFile(uri) + uri?.let { viewModel.updateDataFromFile(uri) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -70,7 +70,10 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) { setupOtherCard(settingsBinding) setupWarrantyCard(settingsBinding) viewModel.stationPosition.asLiveData().observe(viewLifecycleOwner) { stationPos -> - stationPos?.let { handleStationPosition(it, settingsBinding) } + stationPos?.let { handleStationPosition(stationPos, settingsBinding) } + } + viewModel.getUpdateState().asLiveData().observe(viewLifecycleOwner) { updateState -> + updateState?.let { handleSatState(updateState, settingsBinding) } } } @@ -171,13 +174,13 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) { is DataState.Success -> { setPositionText(pos.data, binding) binding.prefsLocation.locationProgress.isIndeterminate = false - showToast(getString(R.string.pref_pos_success)) viewModel.setPositionHandled() + showToast(getString(R.string.pref_pos_success)) } is DataState.Error -> { binding.prefsLocation.locationProgress.isIndeterminate = false - showToast(pos.message.toString()) viewModel.setPositionHandled() + showToast(pos.message.toString()) } DataState.Loading -> { binding.prefsLocation.locationProgress.isIndeterminate = true @@ -186,6 +189,25 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) { } } + private fun handleSatState(state: DataState, binding: FragmentSettingsBinding) { + when (state) { + is DataState.Success -> { + binding.prefsData.updateProgress.isIndeterminate = false + viewModel.setUpdateHandled() + showToast("Data updated successfully") + } + is DataState.Error -> { + binding.prefsData.updateProgress.isIndeterminate = false + viewModel.setUpdateHandled() + showToast(state.message.toString()) + } + is DataState.Loading -> { + binding.prefsData.updateProgress.isIndeterminate = true + } + is DataState.Handled -> {} + } + } + private fun showToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsViewModel.kt index 8ae0de3b..04039f9a 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsViewModel.kt @@ -10,7 +10,6 @@ import com.rtbishop.look4sat.domain.ILocationHandler import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.predict.GeoPos import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -21,14 +20,10 @@ class SettingsViewModel @Inject constructor( private val settings: ISettingsHandler, private val repository: IDataRepository, private val locationHandler: ILocationHandler -) : ViewModel(), ILocationHandler { - - private val exceptionHandler = CoroutineExceptionHandler { _, exception -> - println("Handled $exception in CoroutineExceptionHandler") - } +) : ViewModel() { fun updateDataFromFile(uri: Uri) { - viewModelScope.launch(exceptionHandler) { + viewModelScope.launch { @Suppress("BlockingMethodInNonBlockingContext") resolver.openInputStream(uri)?.use { fileUri -> repository.updateDataFromFile(fileUri) @@ -37,9 +32,7 @@ class SettingsViewModel @Inject constructor( } fun updateDataFromWeb(sources: List) { - viewModelScope.launch(exceptionHandler) { - repository.updateDataFromWeb(sources) - } + repository.updateDataFromWeb(sources) } fun getUseUTC(): Boolean = settings.getUseUTC() @@ -66,21 +59,19 @@ class SettingsViewModel @Inject constructor( fun setRotatorPort(value: String) = settings.setRotatorPort(value) - override val stationPosition: SharedFlow> = locationHandler.stationPosition + fun getUpdateState() = repository.dataUpdateState - override fun getStationPosition(): GeoPos = locationHandler.getStationPosition() + fun setUpdateHandled() = repository.setDataUpdateHandled() - override fun setStationPosition(latitude: Double, longitude: Double) { - locationHandler.setStationPosition(latitude, longitude) - } + val stationPosition: SharedFlow> = locationHandler.stationPosition - override fun setPositionFromGps() = locationHandler.setPositionFromGps() + fun getStationPosition(): GeoPos = locationHandler.getStationPosition() - override fun setPositionFromNet() = locationHandler.setPositionFromNet() + fun setPositionFromGps() = locationHandler.setPositionFromGps() - override fun setPositionFromQth(qthString: String) { - locationHandler.setPositionFromQth(qthString) - } + fun setPositionFromNet() = locationHandler.setPositionFromNet() - override fun setPositionHandled() = locationHandler.setPositionHandled() + fun setPositionFromQth(qthString: String) = locationHandler.setPositionFromQth(qthString) + + fun setPositionHandled() = locationHandler.setPositionHandled() } 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 178fc3e9..f224df5f 100644 --- a/core/src/main/java/com/rtbishop/look4sat/data/DataRepository.kt +++ b/core/src/main/java/com/rtbishop/look4sat/data/DataRepository.kt @@ -19,73 +19,92 @@ package com.rtbishop.look4sat.data import com.rtbishop.look4sat.domain.DataParser import com.rtbishop.look4sat.domain.IDataRepository +import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.SatEntry -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch +import com.rtbishop.look4sat.domain.model.SatItem +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import java.io.InputStream import java.util.zip.ZipInputStream import kotlin.system.measureTimeMillis class DataRepository( private val dataParser: DataParser, + private val settings: ISettingsHandler, private val localSource: ILocalSource, private val remoteSource: IRemoteSource, - private val settingsHandler: ISettingsHandler + private val repositoryScope: CoroutineScope ) : IDataRepository { - override fun getSatelliteItems() = localSource.getSatelliteItems() + private val exceptionHandler = CoroutineExceptionHandler { _, exception -> + println("DataRepository: handled $exception") + _updateState.value = DataState.Error(exception.message) + } + private val _updateState = MutableStateFlow>(DataState.Handled) + override val dataUpdateState: StateFlow> = _updateState + + override fun setDataUpdateHandled() { + _updateState.value = DataState.Handled + } + + override fun updateDataFromFile(stream: InputStream) { + repositoryScope.launch(exceptionHandler) { + _updateState.value = DataState.Loading + val importedSatellites = importSatellites(stream) + localSource.updateEntries(importedSatellites) + _updateState.value = DataState.Success(0L) + } + } + + override fun updateDataFromWeb(sources: List) { + _updateState.value = DataState.Loading + repositoryScope.launch(exceptionHandler) { + settings.saveDataSources(sources) + } + repositoryScope.launch(exceptionHandler) { + val updateTimeMillis = measureTimeMillis { + val jobsMap = mutableMapOf>() + val streamsMap = mutableMapOf() + val streams = mutableListOf() + val entries = mutableListOf() + sources.forEach { jobsMap[it] = async { remoteSource.fetchFileStream(it) } } + jobsMap.forEach { streamsMap[it.key] = it.value.await() } + streamsMap.forEach { stream -> + stream.value?.let { inputStream -> + when { + stream.key.contains("=csv", true) -> { + val tles = dataParser.parseCSVStream(inputStream) + entries.addAll(tles.map { tle -> SatEntry(tle) }) + } + stream.key.contains(".zip", true) -> { + streams.add(ZipInputStream(inputStream).apply { nextEntry }) + } + else -> streams.add(inputStream) + } + } + } + streams.forEach { stream -> entries.addAll(importSatellites(stream)) } + localSource.updateEntries(entries) + } + println("Update from web took $updateTimeMillis ms") + _updateState.value = DataState.Success(updateTimeMillis) + } + repositoryScope.launch(exceptionHandler) { + remoteSource.fetchFileStream(settings.transmittersSource)?.let { stream -> + val transmitters = dataParser.parseJSONStream(stream) + localSource.updateTransmitters(transmitters) + } + } + } + + override fun getSatelliteItems(): Flow> = localSource.getSatelliteItems() override suspend fun getSelectedSatellites() = localSource.getSelectedSatellites() override suspend fun getTransmitters(catnum: Int) = localSource.getTransmitters(catnum) - override suspend fun updateDataFromFile(stream: InputStream) { - localSource.updateEntries(importSatellites(stream)) - } - - override suspend fun updateDataFromWeb(sources: List) { - coroutineScope { - launch { - settingsHandler.saveDataSources(sources) - } - launch { - val updateTimeMillis = measureTimeMillis { - val jobsMap = mutableMapOf>() - val streamsMap = mutableMapOf() - val streams = mutableListOf() - val entries = mutableListOf() - sources.forEach { jobsMap[it] = async { remoteSource.fetchFileStream(it) } } - jobsMap.forEach { streamsMap[it.key] = it.value.await() } - streamsMap.forEach { stream -> - stream.value?.let { inputStream -> - when { - stream.key.contains("=csv", true) -> { - val tles = dataParser.parseCSVStream(inputStream) - entries.addAll(tles.map { tle -> SatEntry(tle) }) - } - stream.key.contains(".zip", true) -> { - streams.add(ZipInputStream(inputStream).apply { nextEntry }) - } - else -> streams.add(inputStream) - } - } - } - streams.forEach { stream -> entries.addAll(importSatellites(stream)) } - localSource.updateEntries(entries) - } - println("Update from web took $updateTimeMillis ms") - } - launch { - remoteSource.fetchFileStream(settingsHandler.transmittersSource)?.let { stream -> - val transmitters = dataParser.parseJSONStream(stream) - localSource.updateTransmitters(transmitters) - } - } - } - } - override suspend fun updateSelection(catnums: List, isSelected: Boolean) { localSource.updateEntriesSelection(catnums, isSelected) } diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/IDataRepository.kt b/core/src/main/java/com/rtbishop/look4sat/domain/IDataRepository.kt index 2742c10d..fc594003 100644 --- a/core/src/main/java/com/rtbishop/look4sat/domain/IDataRepository.kt +++ b/core/src/main/java/com/rtbishop/look4sat/domain/IDataRepository.kt @@ -17,23 +17,29 @@ */ package com.rtbishop.look4sat.domain +import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.model.Transmitter import com.rtbishop.look4sat.domain.predict.Satellite import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import java.io.InputStream interface IDataRepository { + val dataUpdateState: StateFlow> + + fun setDataUpdateHandled() + + fun updateDataFromFile(stream: InputStream) + + fun updateDataFromWeb(sources: List) + fun getSatelliteItems(): Flow> suspend fun getSelectedSatellites(): List suspend fun getTransmitters(catnum: Int): List - suspend fun updateDataFromFile(stream: InputStream) - - suspend fun updateDataFromWeb(sources: List) - suspend fun updateSelection(catnums: List, isSelected: Boolean = true) }