AsyncListDiffer try for satellite passes

This commit is contained in:
Arty Bishop committed 2021-04-02 20:29:57 +01:00
1 parent 6b3a3b31d6
commit 7e97cbea1b
17 files changed
+309 -286

No files matched your search

@@ -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<List<SatTrans>>
@Transaction
@Query("SELECT catNum, name, isSelected FROM entries ORDER BY name ASC")
fun getItemsFlow(): Flow<List<SatItem>>
fun getSatItems(): Flow<List<SatItem>>
@Query("SELECT * FROM transmitters WHERE catNum = :catNum")
fun getSatTransmitters(catNum: Int): Flow<List<SatTrans>>
@Query("SELECT tle FROM entries WHERE isSelected = 1")
suspend fun getSelectedSatellites(): List<Satellite>
@@ -42,8 +40,6 @@ interface SatelliteDao {
@Query("SELECT catNum FROM entries WHERE isSelected = 1")
suspend fun getSelectedCatNums(): List<Int>
// Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEntries(entries: List<SatEntry>)
@@ -53,24 +49,21 @@ interface SatelliteDao {
insertTransmitters(transmitters)
}
@Query("DELETE from transmitters")
suspend fun deleteTransmitters()
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTransmitters(transmitters: List<SatTrans>)
// Update
@Transaction
suspend fun restoreEntriesSelection(catNums: List<Int>, isSelected: Boolean) {
clearEntriesSelection()
catNums.forEach { catNum ->
updateItemSelection(catNum, isSelected)
}
catNums.forEach { catNum -> updateItemSelection(catNum, isSelected) }
}
@Transaction
suspend fun updateEntriesSelection(catNums: List<Int>, 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()
}
@@ -30,7 +30,6 @@ class SelectedSat(
val velocity: Double,
val qthLoc: String,
val osmPos: Position,
val coverage: Double,
val footprint: Overlay,
val groundTrack: Overlay
)
@@ -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<MutableList<SatPass>>>(Result.Success(mutableListOf()))
val passes: StateFlow<Result<MutableList<SatPass>>> = _passes
private val _passes = MutableSharedFlow<List<SatPass>>()
val passes: SharedFlow<List<SatPass>> = _passes
private var selectedSatellites = emptyList<Satellite>()
init {
externalScope.launch { calculatePasses() }
}
suspend fun calculatePasses(satellites: List<Satellite>) {
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<SatPass>()
selectedSatellites.forEach { satellite ->
allPasses.addAll(getPasses(satellite, refDate))
}
val filteredPasses = filterPasses(allPasses, refDate)
_passes.value = Result.Success(filteredPasses)
val allPasses = mutableListOf<SatPass>()
satelliteRepo.getSelectedSatellites().forEach { satellite ->
allPasses.addAll(getPasses(satellite, refDate))
}
_passes.emit(filterPasses(allPasses, refDate))
}
}
private fun getPasses(satellite: Satellite, refDate: Date): MutableList<SatPass> {
val predictor = satellite.getPredictor(prefsRepo.getStationPosition())
val passes = predictor.getPasses(refDate, prefsRepo.getHoursAhead(), true)
return passes as MutableList<SatPass>
private fun getPasses(satellite: Satellite, refDate: Date): List<SatPass> {
val predictor = satellite.getPredictor(prefsManager.getStationPosition())
return predictor.getPasses(refDate, prefsManager.getHoursAhead(), true)
}
private fun filterPasses(passes: MutableList<SatPass>, refDate: Date): MutableList<SatPass> {
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<SatPass>, refDate: Date): List<SatPass> {
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 }
}
}
@@ -46,22 +46,18 @@ class SatelliteRepo @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {
// Query
fun getSatTransmitters(catNum: Int): Flow<List<SatTrans>> {
return satelliteDao.getTransmittersByCatNum(catNum)
fun getSatItems(): Flow<List<SatItem>> {
return satelliteDao.getSatItems()
}
fun getItemsFlow(): Flow<List<SatItem>> {
return satelliteDao.getItemsFlow()
fun getSatTransmitters(catNum: Int): Flow<List<SatTrans>> {
return satelliteDao.getSatTransmitters(catNum)
}
suspend fun getSelectedSatellites(): List<Satellite> {
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<Int>, isSelected: Boolean) {
satelliteDao.updateEntriesSelection(catNums, isSelected)
}
private suspend fun importEntriesFromSources(sources: List<TleSource>) {
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<Int>, isSelected: Boolean) {
satelliteDao.updateEntriesSelection(catNums, isSelected)
}
}
@@ -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
@@ -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)
}
}
@@ -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()
@@ -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
@@ -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<Satellite>()
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
)
}
@@ -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<RecyclerView.ViewHolder>() {
private var satPassList: MutableList<SatPass> = mutableListOf()
private val diffCallback = object : DiffUtil.ItemCallback<SatPass>() {
override fun areItemsTheSame(oldItem: SatPass, newItem: SatPass): Boolean {
return oldItem.catNum == newItem.catNum && oldItem.startDate == newItem.startDate
}
fun setList(list: MutableList<SatPass>) {
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<SatPass>) {
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)
}
}
@@ -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<SatPass>()
private var binding: FragmentPassesBinding? = null
private var passesAdapter: PassesAdapter? = null
private var passes = listOf<SatPass>()
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<List<SatPass>>,
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)
}
}
}
@@ -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<List<SatPass>>>(Result.InProgress)
private var passesCache = emptyList<SatPass>()
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<SatPass>) = 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)
}
}
}
}
@@ -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) =
@@ -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<Preference>(PrefsRepo.keyPositionGPS)?.apply {
findPreference<Preference>(PrefsManager.keyPositionGPS)?.apply {
setOnPreferenceClickListener {
setPositionFromGPS()
return@setOnPreferenceClickListener true
}
}
findPreference<Preference>(PrefsRepo.keyPositionQTH)?.apply {
findPreference<Preference>(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)
@@ -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,
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<List<TleSource>>(sourcesType)
+62 -56
View File
@@ -1,68 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/passes_bar"
style="@style/TopBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/passes_bar"
style="@style/TopBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/passes_timer"
style="@style/TimerText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="4dp"
android:text="@string/timer_zero"
android:textSize="40sp"
app:layout_constraintBottom_toBottomOf="@+id/passes_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/passes_bar"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/passes_timer"
style="@style/TimerText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="4dp"
android:text="@string/timer_zero"
android:textSize="40sp"
app:layout_constraintBottom_toBottomOf="@+id/passes_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/passes_bar"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/passes_refresh"
android:layout_width="64dp"
android:layout_height="48dp"
android:layout_marginEnd="4dp"
android:backgroundTint="@color/greyDark"
android:contentDescription="@string/placeholder"
android:elevation="4dp"
android:src="@drawable/ic_refresh"
app:layout_constraintEnd_toEndOf="@+id/passes_bar"
app:layout_constraintTop_toTopOf="@+id/passes_bar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/passes_recycler"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="2dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passes_bar">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/passes_recycler"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="2dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passes_bar">
</androidx.recyclerview.widget.RecyclerView>
</androidx.recyclerview.widget.RecyclerView>
<ProgressBar
android:id="@+id/passes_progress"
style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/passes_recycler"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passes_bar" />
<ProgressBar
android:id="@+id/passes_progress"
style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/passes_recycler"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passes_bar" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/passes_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/passes_error"
android:textSize="20sp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passes_bar" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/passes_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/passes_error"
android:textSize="20sp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passes_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>