diff --git a/app/src/main/java/com/rtbishop/look4sat/data/database/SatelliteDao.kt b/app/src/main/java/com/rtbishop/look4sat/data/database/SatelliteDao.kt index 24ecdc8c..32649739 100644 --- a/app/src/main/java/com/rtbishop/look4sat/data/database/SatelliteDao.kt +++ b/app/src/main/java/com/rtbishop/look4sat/data/database/SatelliteDao.kt @@ -27,14 +27,12 @@ import kotlinx.coroutines.flow.Flow @Dao interface SatelliteDao { - // Query - - @Query("SELECT * FROM transmitters WHERE catNum = :catNum") - fun getTransmittersByCatNum(catNum: Int): Flow> - @Transaction @Query("SELECT catNum, name, isSelected FROM entries ORDER BY name ASC") - fun getItemsFlow(): Flow> + fun getSatItems(): Flow> + + @Query("SELECT * FROM transmitters WHERE catNum = :catNum") + fun getSatTransmitters(catNum: Int): Flow> @Query("SELECT tle FROM entries WHERE isSelected = 1") suspend fun getSelectedSatellites(): List @@ -42,8 +40,6 @@ interface SatelliteDao { @Query("SELECT catNum FROM entries WHERE isSelected = 1") suspend fun getSelectedCatNums(): List - // Insert - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertEntries(entries: List) @@ -53,24 +49,21 @@ interface SatelliteDao { insertTransmitters(transmitters) } + @Query("DELETE from transmitters") + suspend fun deleteTransmitters() + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertTransmitters(transmitters: List) - // Update - @Transaction suspend fun restoreEntriesSelection(catNums: List, isSelected: Boolean) { clearEntriesSelection() - catNums.forEach { catNum -> - updateItemSelection(catNum, isSelected) - } + catNums.forEach { catNum -> updateItemSelection(catNum, isSelected) } } @Transaction suspend fun updateEntriesSelection(catNums: List, isSelected: Boolean) { - catNums.forEach { catNum -> - updateItemSelection(catNum, isSelected) - } + catNums.forEach { catNum -> updateItemSelection(catNum, isSelected) } } @Query("UPDATE entries SET isSelected = 0") @@ -78,9 +71,4 @@ interface SatelliteDao { @Query("UPDATE entries SET isSelected = :isSelected WHERE catNum = :catNum") suspend fun updateItemSelection(catNum: Int, isSelected: Boolean) - - // Delete - - @Query("DELETE from transmitters") - suspend fun deleteTransmitters() } diff --git a/app/src/main/java/com/rtbishop/look4sat/data/model/SelectedSat.kt b/app/src/main/java/com/rtbishop/look4sat/data/model/SelectedSat.kt index beb09a88..e0f42203 100644 --- a/app/src/main/java/com/rtbishop/look4sat/data/model/SelectedSat.kt +++ b/app/src/main/java/com/rtbishop/look4sat/data/model/SelectedSat.kt @@ -30,7 +30,6 @@ class SelectedSat( val velocity: Double, val qthLoc: String, val osmPos: Position, - val coverage: Double, val footprint: Overlay, val groundTrack: Overlay ) diff --git a/app/src/main/java/com/rtbishop/look4sat/data/repository/PassesRepo.kt b/app/src/main/java/com/rtbishop/look4sat/data/repository/PassesRepo.kt index 1b71ab83..47f4d440 100644 --- a/app/src/main/java/com/rtbishop/look4sat/data/repository/PassesRepo.kt +++ b/app/src/main/java/com/rtbishop/look4sat/data/repository/PassesRepo.kt @@ -18,13 +18,16 @@ package com.rtbishop.look4sat.data.repository import com.github.amsacode.predict4java.Satellite -import com.rtbishop.look4sat.data.model.Result import com.rtbishop.look4sat.data.model.SatPass import com.rtbishop.look4sat.di.DefaultDispatcher +import com.rtbishop.look4sat.di.ExternalScope +import com.rtbishop.look4sat.utility.PrefsManager import com.rtbishop.look4sat.utility.getPredictor import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.* import javax.inject.Inject @@ -32,49 +35,38 @@ import javax.inject.Singleton @Singleton class PassesRepo @Inject constructor( - private val prefsRepo: PrefsRepo, + private val prefsManager: PrefsManager, + private val satelliteRepo: SatelliteRepo, + @ExternalScope private val externalScope: CoroutineScope, @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher ) { - private val _passes = - MutableStateFlow>>(Result.Success(mutableListOf())) - val passes: StateFlow>> = _passes + private val _passes = MutableSharedFlow>() + val passes: SharedFlow> = _passes - private var selectedSatellites = emptyList() + init { + externalScope.launch { calculatePasses() } + } - suspend fun calculatePasses(satellites: List) { + suspend fun calculatePasses(refDate: Date = Date()) { withContext(defaultDispatcher) { - val newCatNums = satellites.map { it.tle.catnum } - val oldCatNums = selectedSatellites.map { it.tle.catnum } - if (newCatNums != oldCatNums) { - _passes.value = Result.InProgress - selectedSatellites = satellites - val refDate = Date(System.currentTimeMillis()) - val allPasses = mutableListOf() - selectedSatellites.forEach { satellite -> - allPasses.addAll(getPasses(satellite, refDate)) - } - val filteredPasses = filterPasses(allPasses, refDate) - _passes.value = Result.Success(filteredPasses) + val allPasses = mutableListOf() + satelliteRepo.getSelectedSatellites().forEach { satellite -> + allPasses.addAll(getPasses(satellite, refDate)) } + _passes.emit(filterPasses(allPasses, refDate)) } } - private fun getPasses(satellite: Satellite, refDate: Date): MutableList { - val predictor = satellite.getPredictor(prefsRepo.getStationPosition()) - val passes = predictor.getPasses(refDate, prefsRepo.getHoursAhead(), true) - return passes as MutableList + private fun getPasses(satellite: Satellite, refDate: Date): List { + val predictor = satellite.getPredictor(prefsManager.getStationPosition()) + return predictor.getPasses(refDate, prefsManager.getHoursAhead(), true) } - private fun filterPasses(passes: MutableList, refDate: Date): MutableList { - val hoursAhead = prefsRepo.getHoursAhead() - val dateFuture = Calendar.getInstance().apply { - this.time = refDate - this.add(Calendar.HOUR, hoursAhead) - }.time - passes.removeAll { it.startDate.after(dateFuture) } - passes.removeAll { it.endDate.before(refDate) } - passes.removeAll { it.maxElevation < prefsRepo.getMinElevation() } - passes.sortBy { it.startDate } - return passes + private fun filterPasses(passes: List, refDate: Date): List { + val timeFuture = Date(refDate.time + (prefsManager.getHoursAhead() * 3600 * 1000)) + return passes.filter { it.endDate.after(refDate) } + .filter { it.startDate.before(timeFuture) } + .filter { it.maxElevation > prefsManager.getMinElevation() } + .sortedBy { it.startDate } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/data/repository/SatelliteRepo.kt b/app/src/main/java/com/rtbishop/look4sat/data/repository/SatelliteRepo.kt index c69812d0..6c344b1d 100644 --- a/app/src/main/java/com/rtbishop/look4sat/data/repository/SatelliteRepo.kt +++ b/app/src/main/java/com/rtbishop/look4sat/data/repository/SatelliteRepo.kt @@ -46,22 +46,18 @@ class SatelliteRepo @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher ) { - // Query - - fun getSatTransmitters(catNum: Int): Flow> { - return satelliteDao.getTransmittersByCatNum(catNum) + fun getSatItems(): Flow> { + return satelliteDao.getSatItems() } - fun getItemsFlow(): Flow> { - return satelliteDao.getItemsFlow() + fun getSatTransmitters(catNum: Int): Flow> { + return satelliteDao.getSatTransmitters(catNum) } suspend fun getSelectedSatellites(): List { return satelliteDao.getSelectedSatellites() } - // Insert - suspend fun importSatDataFromFile(uri: Uri) { runCatching { resolver.openInputStream(uri)?.use { stream -> @@ -80,6 +76,10 @@ class SatelliteRepo @Inject constructor( } } + suspend fun updateEntriesSelection(catNums: List, isSelected: Boolean) { + satelliteDao.updateEntriesSelection(catNums, isSelected) + } + private suspend fun importEntriesFromSources(sources: List) { val streams = getStreamsForSources(sources) val entries = importEntriesFromStreams(streams) @@ -133,10 +133,4 @@ class SatelliteRepo @Inject constructor( satelliteDao.insertEntries(entries) satelliteDao.restoreEntriesSelection(selectedCatNums, true) } - - // Update - - suspend fun updateEntriesSelection(catNums: List, isSelected: Boolean) { - satelliteDao.updateEntriesSelection(catNums, isSelected) - } } diff --git a/app/src/main/java/com/rtbishop/look4sat/di/CoroutinesModule.kt b/app/src/main/java/com/rtbishop/look4sat/di/CoroutinesModule.kt index fd86bf08..1688cac0 100644 --- a/app/src/main/java/com/rtbishop/look4sat/di/CoroutinesModule.kt +++ b/app/src/main/java/com/rtbishop/look4sat/di/CoroutinesModule.kt @@ -22,13 +22,21 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher +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 + @ExternalScope + fun providesExternalScope(): CoroutineScope = CoroutineScope(SupervisorJob()) + @Provides @DefaultDispatcher fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default @@ -42,6 +50,10 @@ object CoroutinesModule { fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main } +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ExternalScope + @Qualifier @Retention(AnnotationRetention.BINARY) annotation class DefaultDispatcher diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/Look4SatActivity.kt b/app/src/main/java/com/rtbishop/look4sat/ui/Look4SatActivity.kt index 9d42639c..0d468577 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/Look4SatActivity.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/Look4SatActivity.kt @@ -22,7 +22,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import com.rtbishop.look4sat.R -import com.rtbishop.look4sat.data.repository.PrefsRepo +import com.rtbishop.look4sat.utility.PrefsManager import com.rtbishop.look4sat.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -31,7 +31,7 @@ import javax.inject.Inject class Look4SatActivity : AppCompatActivity() { @Inject - lateinit var prefsRepo: PrefsRepo + lateinit var prefsManager: PrefsManager override fun onCreate(savedInstanceState: Bundle?) { setTheme(R.style.AppTheme) @@ -40,9 +40,8 @@ class Look4SatActivity : AppCompatActivity() { setContentView(root) val navHost = supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment navBottom.setupWithNavController(navHost.navController) - - if (prefsRepo.isFirstLaunch()) { - prefsRepo.setFirstLaunchDone() + if (prefsManager.isFirstLaunch()) { + prefsManager.setFirstLaunchDone() navHost.navController.navigate(R.id.nav_dialog_splash) } } diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesViewModel.kt index bc78756f..e151999f 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesViewModel.kt @@ -27,7 +27,7 @@ import com.rtbishop.look4sat.R import com.rtbishop.look4sat.data.model.Result import com.rtbishop.look4sat.data.model.SatItem import com.rtbishop.look4sat.data.model.TleSource -import com.rtbishop.look4sat.data.repository.PrefsRepo +import com.rtbishop.look4sat.utility.PrefsManager import com.rtbishop.look4sat.data.repository.SatelliteRepo import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collect @@ -39,14 +39,14 @@ import kotlin.system.measureTimeMillis @HiltViewModel class EntriesViewModel @Inject constructor( - private val prefsRepo: PrefsRepo, + private val prefsManager: PrefsManager, private val satelliteRepo: SatelliteRepo ) : ViewModel(), EntriesAdapter.EntriesClickListener, SearchView.OnQueryTextListener { - private val transModes = MutableLiveData(prefsRepo.loadModesSelection()) + private val transModes = MutableLiveData(prefsManager.loadModesSelection()) private val currentQuery = MutableLiveData(String()) private val itemsWithModes = transModes.switchMap { modes -> - liveData { satelliteRepo.getItemsFlow().collect { emit(filterByModes(it, modes)) } } + liveData { satelliteRepo.getSatItems().collect { emit(filterByModes(it, modes)) } } } private val itemsWithQuery = currentQuery.switchMap { query -> itemsWithModes.map { items -> Result.Success(filterByQuery(items, query)) } @@ -72,10 +72,10 @@ class EntriesViewModel @Inject constructor( viewModelScope.launch { _satData.value = Result.InProgress val satSources = if (sources.isNotEmpty()) sources - else prefsRepo.loadDefaultSources() + else prefsManager.loadDefaultSources() val updateMillis = measureTimeMillis { try { - prefsRepo.saveTleSources(satSources) + prefsManager.saveTleSources(satSources) satelliteRepo.importSatDataFromWeb(satSources) } catch (exception: Exception) { _satData.value = Result.Error(exception) @@ -108,7 +108,7 @@ class EntriesViewModel @Inject constructor( "PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT" ) val savedModes = BooleanArray(modes.size) - val selectedModes = prefsRepo.loadModesSelection().toMutableList() + val selectedModes = prefsManager.loadModesSelection().toMutableList() selectedModes.forEach { savedModes[modes.indexOf(it)] = true } val dialogBuilder = MaterialAlertDialogBuilder(context).apply { setTitle(context.getString(R.string.modes_title)) @@ -120,7 +120,7 @@ class EntriesViewModel @Inject constructor( } setPositiveButton(context.getString(android.R.string.ok)) { _, _ -> transModes.value = selectedModes - prefsRepo.saveModesSelection(selectedModes) + prefsManager.saveModesSelection(selectedModes) } setNeutralButton(context.getString(android.R.string.cancel)) { dialog, _ -> dialog.dismiss() diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/mapScreen/MapFragment.kt b/app/src/main/java/com/rtbishop/look4sat/ui/mapScreen/MapFragment.kt index 324521fd..64fe2a3a 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/mapScreen/MapFragment.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/mapScreen/MapFragment.kt @@ -29,7 +29,7 @@ import com.github.amsacode.predict4java.Position import com.github.amsacode.predict4java.Satellite import com.rtbishop.look4sat.R import com.rtbishop.look4sat.data.model.SelectedSat -import com.rtbishop.look4sat.data.repository.PrefsRepo +import com.rtbishop.look4sat.utility.PrefsManager import com.rtbishop.look4sat.databinding.FragmentMapBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.FlowPreview @@ -50,7 +50,7 @@ import javax.inject.Inject class MapFragment : Fragment(R.layout.fragment_map) { @Inject - lateinit var prefsRepo: PrefsRepo + lateinit var prefsManager: PrefsManager private lateinit var binding: FragmentMapBinding private val mapViewModel: MapViewModel by viewModels() private val minLat = MapView.getTileSystem().minLatitude @@ -58,7 +58,7 @@ class MapFragment : Fragment(R.layout.fragment_map) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Configuration.getInstance().load(requireContext(), prefsRepo.preferences) + Configuration.getInstance().load(requireContext(), prefsManager.preferences) binding = FragmentMapBinding.bind(view).apply { mapView.apply { setMultiTouchControls(true) @@ -119,7 +119,7 @@ class MapFragment : Fragment(R.layout.fragment_map) { binding.apply { val markers = FolderOverlay() map.entries.forEach { - if (prefsRepo.shouldUseTextLabels()) { + if (prefsManager.shouldUseTextLabels()) { Marker(mapView).apply { setInfoWindow(null) textLabelFontSize = 24 diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/mapScreen/MapViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/ui/mapScreen/MapViewModel.kt index 2d3d70d1..7e6b9a79 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/mapScreen/MapViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/mapScreen/MapViewModel.kt @@ -24,7 +24,7 @@ import com.github.amsacode.predict4java.Position import com.github.amsacode.predict4java.SatPos import com.github.amsacode.predict4java.Satellite import com.rtbishop.look4sat.data.model.SelectedSat -import com.rtbishop.look4sat.data.repository.PrefsRepo +import com.rtbishop.look4sat.utility.PrefsManager import com.rtbishop.look4sat.data.repository.SatelliteRepo import com.rtbishop.look4sat.utility.* import dagger.hilt.android.lifecycle.HiltViewModel @@ -39,7 +39,7 @@ import javax.inject.Inject @HiltViewModel class MapViewModel @Inject constructor( - prefsRepo: PrefsRepo, + prefsManager: PrefsManager, private val satelliteRepo: SatelliteRepo, private val qthConverter: QthConverter ) : ViewModel() { @@ -59,7 +59,7 @@ class MapViewModel @Inject constructor( isAntiAlias = true } - private val gsp = prefsRepo.getStationPosition() + private val gsp = prefsManager.getStationPosition() private var filteredSats = listOf() private lateinit var selectedSat: Satellite @@ -118,12 +118,11 @@ class MapViewModel @Inject constructor( ) val qthLoc = qthConverter.locationToQTH(osmPos.lat, osmPos.lon) ?: "-- --" val velocity = satPos.altitude.getOrbitalVelocity() - val coverage = satPos.rangeCircleRadiusKm * 2 val footprint = getSatFootprint(satPos) val track = getSatTrack(satellite) return@withContext SelectedSat( satellite, satellite.tle.catnum, satellite.tle.name, satPos.range, - satPos.altitude, velocity, qthLoc, osmPos, coverage, footprint, track + satPos.altitude, velocity, qthLoc, osmPos, footprint, track ) } diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesAdapter.kt b/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesAdapter.kt index 411c6d7d..01b00518 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesAdapter.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesAdapter.kt @@ -19,57 +19,47 @@ package com.rtbishop.look4sat.ui.passesScreen import android.view.LayoutInflater import android.view.ViewGroup -import androidx.core.os.bundleOf -import androidx.navigation.findNavController +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.rtbishop.look4sat.R import com.rtbishop.look4sat.data.model.SatPass import com.rtbishop.look4sat.databinding.ItemPassGeoBinding import com.rtbishop.look4sat.databinding.ItemPassLeoBinding -import com.rtbishop.look4sat.utility.navigateSafe import java.text.SimpleDateFormat import java.util.* class PassesAdapter(private val shouldUseUTC: Boolean = false) : RecyclerView.Adapter() { - private var satPassList: MutableList = mutableListOf() + private val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SatPass, newItem: SatPass): Boolean { + return oldItem.catNum == newItem.catNum && oldItem.startDate == newItem.startDate + } - fun setList(list: MutableList) { - satPassList = list - notifyDataSetChanged() - } - - fun tickPasses(timeNow: Long) { - val iterator = satPassList.listIterator() - while (iterator.hasNext()) { - val satPass = iterator.next() - if (!satPass.isDeepSpace) { - if (satPass.progress < 100) { - val timeStart = satPass.startDate.time - if (timeNow > timeStart) { - val timeEnd = satPass.endDate.time - val index = satPassList.indexOf(satPass) - val deltaNow = timeNow.minus(timeStart).toFloat() - val deltaTotal = timeEnd.minus(timeStart).toFloat() - satPass.progress = ((deltaNow / deltaTotal) * 100).toInt() - notifyItemChanged(index) - } - } else { - val index = satPassList.indexOf(satPass) - iterator.remove() - notifyItemRemoved(index) - } - } + override fun areContentsTheSame(oldItem: SatPass, newItem: SatPass): Boolean { + return false } } + private val listDiffer = AsyncListDiffer(this, diffCallback) + private lateinit var passesClickListener: PassesClickListener - override fun getItemCount(): Int { - return satPassList.size + interface PassesClickListener { + fun navigateToPass(satPass: SatPass) } + fun setPassesClickListener(listener: PassesClickListener) { + passesClickListener = listener + } + + fun submitPasses(items: List) { + listDiffer.submitList(items) + } + + override fun getItemCount() = listDiffer.currentList.size + override fun getItemViewType(position: Int): Int { - return if (satPassList[position].isDeepSpace) 1 + return if (listDiffer.currentList[position].isDeepSpace) 1 else 0 } @@ -83,9 +73,11 @@ class PassesAdapter(private val shouldUseUTC: Boolean = false) : override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (holder.itemViewType == 0) { - (holder as SatPassLeoHolder).bind(satPassList[position], position, shouldUseUTC) + (holder as SatPassLeoHolder) + .bind(listDiffer.currentList[position], shouldUseUTC, passesClickListener) } else { - (holder as SatPassGeoHolder).bind(satPassList[position], position) + (holder as SatPassGeoHolder) + .bind(listDiffer.currentList[position], passesClickListener) } } @@ -103,7 +95,7 @@ class PassesAdapter(private val shouldUseUTC: Boolean = false) : private val startFormat = SimpleDateFormat(startTimeFormat, Locale.getDefault()) private val endFormat = SimpleDateFormat(endTimeFormat, Locale.getDefault()) - fun bind(satPass: SatPass, position: Int, shouldUseUTC: Boolean) { + fun bind(satPass: SatPass, shouldUseUTC: Boolean, listener: PassesClickListener) { binding.apply { if (shouldUseUTC) { startFormat.timeZone = timeZoneUTC @@ -119,12 +111,8 @@ class PassesAdapter(private val shouldUseUTC: Boolean = false) : passLeoEnd.text = endFormat.format(satPass.endDate) passLeoProgress.progress = satPass.progress } - itemView.setOnClickListener { - if (satPass.progress < 100) { - val bundle = bundleOf("index" to position) - itemView.findNavController().navigateSafe(R.id.action_passes_to_polar, bundle) - } + listener.navigateToPass(satPass) } } @@ -144,7 +132,7 @@ class PassesAdapter(private val shouldUseUTC: Boolean = false) : private val altFormat = itemView.context.getString(R.string.pat_altitude) private val elevFormat = itemView.context.getString(R.string.pat_elevation) - fun bind(satPass: SatPass, position: Int) { + fun bind(satPass: SatPass, listener: PassesClickListener) { binding.apply { passGeoSatName.text = satPass.name passGeoSatId.text = String.format(satIdFormat, satPass.catNum) @@ -152,10 +140,8 @@ class PassesAdapter(private val shouldUseUTC: Boolean = false) : passGeoAlt.text = String.format(altFormat, satPass.altitude) passGeoEl.text = String.format(elevFormat, satPass.maxElevation) } - itemView.setOnClickListener { - val bundle = bundleOf("index" to position) - itemView.findNavController().navigateSafe(R.id.action_passes_to_polar, bundle) + listener.navigateToPass(satPass) } } diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesFragment.kt b/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesFragment.kt index 5de59a12..55880deb 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesFragment.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesFragment.kt @@ -19,8 +19,10 @@ package com.rtbishop.look4sat.ui.passesScreen import android.os.Bundle import android.view.View +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.SimpleItemAnimator import com.rtbishop.look4sat.R @@ -29,63 +31,26 @@ import com.rtbishop.look4sat.data.model.SatPass import com.rtbishop.look4sat.databinding.FragmentPassesBinding import com.rtbishop.look4sat.utility.RecyclerDivider import com.rtbishop.look4sat.utility.formatForTimer +import com.rtbishop.look4sat.utility.navigateSafe import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.FlowPreview import java.util.* -@FlowPreview @AndroidEntryPoint -class PassesFragment : Fragment(R.layout.fragment_passes) { +class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesClickListener { private val viewModel: PassesViewModel by viewModels() - private var passes = mutableListOf() - private var binding: FragmentPassesBinding? = null - private var passesAdapter: PassesAdapter? = null + private var passes = listOf() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding = FragmentPassesBinding.bind(view) - setupComponents() - setupObservers() + setupComponents(view) } - private fun setupObservers() { - viewModel.passes.observe(viewLifecycleOwner, { result -> - if (result is Result.Success) { - if (result.data.isEmpty()) { - binding?.apply { - passesProgress.visibility = View.INVISIBLE - passesRecycler.visibility = View.INVISIBLE - passesError.visibility = View.VISIBLE - } - } else { - passes = result.data - passesAdapter?.setList(passes) - binding?.apply { - passesError.visibility = View.INVISIBLE - passesProgress.visibility = View.INVISIBLE - passesRecycler.visibility = View.VISIBLE - } - } - } else if (result is Result.InProgress) { - passes.clear() - binding?.apply { - passesTimer.text = 0L.formatForTimer() - passesError.visibility = View.INVISIBLE - passesRecycler.visibility = View.INVISIBLE - passesProgress.visibility = View.VISIBLE - } - } - }) - viewModel.getAppTimer().observe(viewLifecycleOwner, { currentTime -> - tickMainTimer(currentTime) - passesAdapter?.tickPasses(currentTime) - }) - } - - private fun setupComponents() { - passesAdapter = PassesAdapter(viewModel.shouldUseUTC()) - binding?.apply { + private fun setupComponents(view: View) { + val passesAdapter = PassesAdapter(viewModel.shouldUseUTC()).apply { + setPassesClickListener(this@PassesFragment) + } + val binding = FragmentPassesBinding.bind(view).apply { passesRecycler.apply { setHasFixedSize(true) adapter = passesAdapter @@ -94,28 +59,68 @@ class PassesFragment : Fragment(R.layout.fragment_passes) { (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false addItemDecoration(RecyclerDivider(R.drawable.rec_divider_dark)) } + passesRefresh.setOnClickListener { viewModel.forceCalculation() } + } + viewModel.passes.observe(viewLifecycleOwner, { result -> + handlePassesResult(result, binding, passesAdapter) + }) + viewModel.getAppTimer().observe(viewLifecycleOwner, { currentTime -> + tickMainTimer(currentTime, binding) + }) + } + + private fun handlePassesResult( + result: Result>, + binding: FragmentPassesBinding, + passesAdapter: PassesAdapter + ) { + if (result is Result.Success) { + if (result.data.isEmpty()) { + binding.apply { + passesProgress.visibility = View.INVISIBLE + passesRecycler.visibility = View.INVISIBLE + passesError.visibility = View.VISIBLE + passesTimer.text = 0L.formatForTimer() + } + } else { + passes = result.data + passesAdapter.submitPasses(result.data) + binding.apply { + passesError.visibility = View.INVISIBLE + passesProgress.visibility = View.INVISIBLE + passesRecycler.visibility = View.VISIBLE + } + } + } else if (result is Result.InProgress) { + binding.apply { + passesTimer.text = 0L.formatForTimer() + passesError.visibility = View.INVISIBLE + passesRecycler.visibility = View.INVISIBLE + passesProgress.visibility = View.VISIBLE + } } } - private fun tickMainTimer(timeNow: Long) { + private fun tickMainTimer(timeNow: Long, binding: FragmentPassesBinding) { if (passes.isNotEmpty()) { try { val nextPass = passes.first { it.startDate.time.minus(timeNow) > 0 } val millisBeforeStart = nextPass.startDate.time.minus(timeNow) - binding?.passesTimer?.text = millisBeforeStart.formatForTimer() + binding.passesTimer.text = millisBeforeStart.formatForTimer() } catch (e: NoSuchElementException) { val lastPass = passes.last() val millisBeforeEnd = lastPass.endDate.time.minus(timeNow) - binding?.passesTimer?.text = millisBeforeEnd.formatForTimer() + binding.passesTimer.text = millisBeforeEnd.formatForTimer() } } else { - binding?.passesTimer?.text = 0L.formatForTimer() + binding.passesTimer.text = 0L.formatForTimer() } } - override fun onDestroyView() { - passesAdapter = null - binding = null - super.onDestroyView() + override fun navigateToPass(satPass: SatPass) { + if (satPass.progress <= 100) { + val bundle = bundleOf("index" to passes.indexOf(satPass)) + findNavController().navigateSafe(R.id.action_passes_to_polar, bundle) + } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesViewModel.kt index ac1918ca..21a59179 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/passesScreen/PassesViewModel.kt @@ -21,28 +21,29 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope +import com.rtbishop.look4sat.data.model.Result +import com.rtbishop.look4sat.data.model.SatPass import com.rtbishop.look4sat.data.repository.PassesRepo -import com.rtbishop.look4sat.data.repository.PrefsRepo -import com.rtbishop.look4sat.data.repository.SatelliteRepo +import com.rtbishop.look4sat.di.DefaultDispatcher +import com.rtbishop.look4sat.utility.PrefsManager import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import timber.log.Timber import javax.inject.Inject @HiltViewModel class PassesViewModel @Inject constructor( - private val prefsRepo: PrefsRepo, - private val satelliteRepo: SatelliteRepo, - private val passesRepo: PassesRepo + private val passesRepo: PassesRepo, + private val prefsManager: PrefsManager, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher ) : ViewModel() { - val passes = passesRepo.passes.asLiveData(viewModelScope.coroutineContext) - - init { - viewModelScope.launch { - passesRepo.calculatePasses(satelliteRepo.getSelectedSatellites()) - } - } + private val _passes = MutableStateFlow>>(Result.InProgress) + private var passesCache = emptyList() + private var passesCalculation: Job? = null + val passes = _passes.asLiveData(viewModelScope.coroutineContext) fun getAppTimer() = liveData { while (true) { @@ -52,6 +53,53 @@ class PassesViewModel @Inject constructor( } fun shouldUseUTC(): Boolean { - return prefsRepo.shouldUseUTC() + return prefsManager.shouldUseUTC() + } + + init { + viewModelScope.launch { + passesRepo.passes.collect { newPasses -> + passesCalculation?.cancelAndJoin() + passesCalculation = launch { handlePasses(newPasses) } + } + } + } + + fun forceCalculation() { + viewModelScope.launch { + _passes.value = Result.InProgress + passesRepo.calculatePasses() + } + } + + private suspend fun handlePasses(newPasses: List) = withContext(defaultDispatcher) { + if (newPasses.isEmpty()) { + _passes.value = Result.Success(newPasses) + } else { + while (isActive) { + val timeNow = System.currentTimeMillis() + passesCache = newPasses.toMutableList().apply { + val iterator = listIterator() + while (iterator.hasNext()) { + val satPass = iterator.next() + if (!satPass.isDeepSpace) { + if (satPass.progress <= 100) { + val timeStart = satPass.startDate.time + if (timeNow > timeStart) { + val deltaNow = timeNow.minus(timeStart).toFloat() + val deltaTotal = satPass.endDate.time.minus(timeStart).toFloat() + satPass.progress = ((deltaNow / deltaTotal) * 100).toInt() + } + } else { + iterator.remove() + } + } + } + } + _passes.value = Result.Success(passesCache) + Timber.d("$passesCache") + delay(1000) + } + } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/polarScreen/PolarViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/ui/polarScreen/PolarViewModel.kt index fb119e47..0b36f6c0 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/polarScreen/PolarViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/polarScreen/PolarViewModel.kt @@ -21,10 +21,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope -import com.rtbishop.look4sat.data.model.Result import com.rtbishop.look4sat.data.repository.PassesRepo -import com.rtbishop.look4sat.data.repository.PrefsRepo import com.rtbishop.look4sat.data.repository.SatelliteRepo +import com.rtbishop.look4sat.utility.PrefsManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect @@ -32,7 +31,7 @@ import javax.inject.Inject @HiltViewModel class PolarViewModel @Inject constructor( - private val prefsRepo: PrefsRepo, + private val prefsManager: PrefsManager, private val passesRepo: PassesRepo, private val satelliteRepo: SatelliteRepo ) : ViewModel() { @@ -45,19 +44,15 @@ class PolarViewModel @Inject constructor( } fun getPass(passId: Int) = liveData { - passesRepo.passes.collect { passes -> - if (passes is Result.Success) { - emit(passes.data[passId]) - } - } + passesRepo.passes.collect { passes -> emit(passes[passId]) } } fun shouldUseCompass(): Boolean { - return prefsRepo.shouldUseCompass() + return prefsManager.shouldUseCompass() } fun getMagDeclination(): Float { - return prefsRepo.getMagDeclination() + return prefsManager.getMagDeclination() } fun getSatTransmitters(satId: Int) = diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/prefsScreen/PrefsFragment.kt b/app/src/main/java/com/rtbishop/look4sat/ui/prefsScreen/PrefsFragment.kt index 952410cf..5b0eb9a2 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/prefsScreen/PrefsFragment.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/prefsScreen/PrefsFragment.kt @@ -28,7 +28,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.snackbar.Snackbar import com.rtbishop.look4sat.R -import com.rtbishop.look4sat.data.repository.PrefsRepo +import com.rtbishop.look4sat.utility.PrefsManager import com.rtbishop.look4sat.utility.QthConverter import com.rtbishop.look4sat.utility.round import dagger.hilt.android.AndroidEntryPoint @@ -41,7 +41,7 @@ class PrefsFragment : PreferenceFragmentCompat() { lateinit var locationManager: LocationManager @Inject - lateinit var prefsRepo: PrefsRepo + lateinit var prefsManager: PrefsManager @Inject lateinit var qthConverter: QthConverter @@ -62,14 +62,14 @@ class PrefsFragment : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - findPreference(PrefsRepo.keyPositionGPS)?.apply { + findPreference(PrefsManager.keyPositionGPS)?.apply { setOnPreferenceClickListener { setPositionFromGPS() return@setOnPreferenceClickListener true } } - findPreference(PrefsRepo.keyPositionQTH)?.apply { + findPreference(PrefsManager.keyPositionQTH)?.apply { setOnPreferenceChangeListener { _, newValue -> setPositionFromQth(newValue.toString()) } @@ -78,7 +78,7 @@ class PrefsFragment : PreferenceFragmentCompat() { private fun setPositionFromQth(qthString: String): Boolean { qthConverter.qthToLocation(qthString)?.let { gsp -> - prefsRepo.setStationPosition(gsp.latitude, gsp.longitude, gsp.heightAMSL) + prefsManager.setStationPosition(gsp.latitude, gsp.longitude, gsp.heightAMSL) showSnack(getString(R.string.pref_pos_success)) return true } @@ -95,7 +95,7 @@ class PrefsFragment : PreferenceFragmentCompat() { val latitude = location.latitude.round(4) val longitude = location.longitude.round(4) val altitude = location.altitude.round(1) - prefsRepo.setStationPosition(latitude, longitude, altitude) + prefsManager.setStationPosition(latitude, longitude, altitude) showSnack(getString(R.string.pref_pos_success)) } else showSnack(getString(R.string.pref_pos_gps_null)) } else requestPermissionLauncher.launch(locPermString) diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/sourcesDialog/SourcesDialog.kt b/app/src/main/java/com/rtbishop/look4sat/ui/sourcesDialog/SourcesDialog.kt index c9eb6bf7..f751efdb 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/sourcesDialog/SourcesDialog.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/sourcesDialog/SourcesDialog.kt @@ -25,7 +25,7 @@ import android.view.WindowManager import androidx.appcompat.app.AppCompatDialogFragment import androidx.recyclerview.widget.LinearLayoutManager import com.rtbishop.look4sat.R -import com.rtbishop.look4sat.data.repository.PrefsRepo +import com.rtbishop.look4sat.utility.PrefsManager import com.rtbishop.look4sat.databinding.DialogSourcesBinding import com.rtbishop.look4sat.utility.setNavResult import dagger.hilt.android.AndroidEntryPoint @@ -35,7 +35,7 @@ import javax.inject.Inject class SourcesDialog : AppCompatDialogFragment() { @Inject - lateinit var prefsRepo: PrefsRepo + lateinit var prefsManager: PrefsManager override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? { return inflater.inflate(R.layout.dialog_sources, group, false) @@ -43,7 +43,7 @@ class SourcesDialog : AppCompatDialogFragment() { override fun onViewCreated(view: View, state: Bundle?) { super.onViewCreated(view, state) - val sourcesAdapter = SourcesAdapter().apply { setSources(prefsRepo.loadTleSources()) } + val sourcesAdapter = SourcesAdapter().apply { setSources(prefsManager.loadTleSources()) } DialogSourcesBinding.bind(view).apply { dialog?.window?.setLayout( WindowManager.LayoutParams.MATCH_PARENT, diff --git a/app/src/main/java/com/rtbishop/look4sat/data/repository/PrefsRepo.kt b/app/src/main/java/com/rtbishop/look4sat/utility/PrefsManager.kt similarity index 97% rename from app/src/main/java/com/rtbishop/look4sat/data/repository/PrefsRepo.kt rename to app/src/main/java/com/rtbishop/look4sat/utility/PrefsManager.kt index 8b6fcf7c..acb71bf1 100644 --- a/app/src/main/java/com/rtbishop/look4sat/data/repository/PrefsRepo.kt +++ b/app/src/main/java/com/rtbishop/look4sat/utility/PrefsManager.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.rtbishop.look4sat.data.repository +package com.rtbishop.look4sat.utility import android.content.SharedPreferences import android.hardware.GeomagneticField @@ -28,7 +28,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class PrefsRepo @Inject constructor(val preferences: SharedPreferences, moshi: Moshi) { +class PrefsManager @Inject constructor(val preferences: SharedPreferences, moshi: Moshi) { private val sourcesType = Types.newParameterizedType(List::class.java, TleSource::class.java) private val sourcesAdapter = moshi.adapter>(sourcesType) diff --git a/app/src/main/res/layout/fragment_passes.xml b/app/src/main/res/layout/fragment_passes.xml index c3cc0e16..1dc726e6 100644 --- a/app/src/main/res/layout/fragment_passes.xml +++ b/app/src/main/res/layout/fragment_passes.xml @@ -1,68 +1,74 @@ - - + - + - + - + - + - + - + - - - +