Code separation and cleanup for future testing

pull/30/head
Arty Bishop 2020-03-01 21:42:50 +00:00
rodzic 38b9feb829
commit b5fa5c0673
10 zmienionych plików z 311 dodań i 152 usunięć

Wyświetl plik

@ -20,11 +20,6 @@
package com.rtbishop.look4sat
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.location.LocationManager
import android.util.Log
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@ -36,103 +31,58 @@ import com.rtbishop.look4sat.predict4kotlin.PassPredictor
import com.rtbishop.look4sat.repo.Repository
import com.rtbishop.look4sat.repo.SatPass
import com.rtbishop.look4sat.repo.Transmitter
import com.rtbishop.look4sat.utility.DataManager
import com.rtbishop.look4sat.utility.PrefsManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.*
import javax.inject.Inject
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val tag = "MainViewModel"
private val app = application
private val keyHours = application.getString(R.string.pref_hours_ahead_key)
private val keyMinEl = application.getString(R.string.pref_min_el_key)
private val keyLat = application.getString(R.string.pref_lat_key)
private val keyLon = application.getString(R.string.pref_lon_key)
private val keyAlt = application.getString(R.string.pref_alt_key)
private val keyDelay = application.getString(R.string.pref_refresh_rate_key)
private val tleMainListFileName = "tleFile.txt"
private val tleSelectionFileName = "tleSelection"
private val _debugMessage = MutableLiveData<String>()
val debugMessage: LiveData<String> = _debugMessage
private val _satPassList = MutableLiveData<MutableList<SatPass>>()
val satPassList: LiveData<MutableList<SatPass>> = _satPassList
private val _gsp = MutableLiveData<GroundStationPosition>()
val gsp: LiveData<GroundStationPosition> = _gsp
private val urlList = listOf("https://celestrak.com/NORAD/elements/active.txt")
private val _satPassList = MutableLiveData<MutableList<SatPass>>()
private val _debugMessage = MutableLiveData<String>()
private val _gsp = MutableLiveData<GroundStationPosition>()
private var calculationJob: Job? = null
@Inject
lateinit var locationManager: LocationManager
@Inject
lateinit var preferences: SharedPreferences
@Inject
lateinit var repository: Repository
lateinit var tleMainList: List<TLE>
lateinit var tleSelection: MutableList<Int>
@Inject
lateinit var dataManager: DataManager
@Inject
lateinit var prefsManager: PrefsManager
init {
(app as Look4SatApp).appComponent.inject(this)
loadDataFromDisk()
setGroundStationPosition()
}
val delay: Long
get() = preferences.getString(keyDelay, "3000")!!.toLong()
var tleMainList = dataManager.loadTleList()
var tleSelection = dataManager.loadSelectionList()
val hoursAhead: Int
get() = preferences.getInt(keyHours, 8)
val minEl: Double
get() = preferences.getDouble(keyMinEl, 16.0)
fun setGroundStationPosition() {
val lat = preferences.getString(keyLat, "0.0")!!.toDouble()
val lon = preferences.getString(keyLon, "0.0")!!.toDouble()
val alt = preferences.getString(keyAlt, "0.0")!!.toDouble()
_gsp.postValue(GroundStationPosition(lat, lon, alt))
}
fun getGSP(): LiveData<GroundStationPosition> = _gsp
fun getDebugMessage(): LiveData<String> = _debugMessage
fun getSatPassList(): LiveData<MutableList<SatPass>> = _satPassList
fun getRefreshRate() = prefsManager.getRefreshRate()
fun getHoursAhead() = prefsManager.getHoursAhead()
fun getMinElevation() = prefsManager.getMinElevation()
fun setGroundStationPosition() = _gsp.postValue(prefsManager.getGroundStationPosition())
fun updateLocation() {
val passiveProvider = LocationManager.PASSIVE_PROVIDER
try {
val location = locationManager.getLastKnownLocation(passiveProvider)
val lat = location?.latitude ?: 0.0
val lon = location?.longitude ?: 0.0
val alt = location?.altitude ?: 0.0
preferences.edit {
putString(keyLat, lat.toString())
putString(keyLon, lon.toString())
putString(keyAlt, alt.toString())
apply()
}
_gsp.postValue(GroundStationPosition(lat, lon, alt))
_debugMessage.postValue(app.getString(R.string.update_loc_success))
} catch (e: SecurityException) {
_debugMessage.postValue(app.getString(R.string.err_no_permissions))
dataManager.getLastKnownLocation()?.let {
prefsManager.setGroundStationPosition(it)
_gsp.postValue(it)
}
}
fun setPassPrefs(hoursAhead: Int, minEl: Double) {
preferences.edit {
putInt(keyHours, hoursAhead)
putDouble(keyMinEl, minEl)
apply()
}
prefsManager.setHoursAhead(hoursAhead)
prefsManager.setMinElevation(minEl)
}
fun updateAndSaveTleFile() {
@ -140,12 +90,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
try {
val inputStream = repository.getStreamForUrl(urlList)
val tleList = TLE.importSat(inputStream).apply { sortBy { it.name } }
val fileOutStream = app.openFileOutput(tleMainListFileName, Context.MODE_PRIVATE)
ObjectOutputStream(fileOutStream).apply {
writeObject(tleList)
flush()
close()
}
dataManager.saveTleList(tleList)
tleMainList = tleList
tleSelection = mutableListOf()
_debugMessage.postValue(app.getString(R.string.update_tle_success))
@ -168,35 +113,21 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun updateAndSaveSelectionList(list: MutableList<Int>) {
tleSelection = list
try {
val fileOutputStream = app.openFileOutput(tleSelectionFileName, Context.MODE_PRIVATE)
ObjectOutputStream(fileOutputStream).apply {
writeObject(list)
flush()
close()
}
} catch (exception: IOException) {
_debugMessage.postValue(exception.toString())
}
dataManager.saveSelectionList(list)
}
fun getPasses() {
calculationJob?.cancel()
val passList = mutableListOf<SatPass>()
val dateNow = Date()
val dateFuture = Calendar.getInstance().let {
it.time = dateNow
it.add(Calendar.HOUR, hoursAhead)
it.time
}
var passList = mutableListOf<SatPass>()
calculationJob = viewModelScope.launch {
if (tleMainList.isNotEmpty() && tleSelection.isNotEmpty()) {
withContext(Dispatchers.Default) {
val dateNow = Date()
tleSelection.forEach { indexOfSelection ->
val tle = tleMainList[indexOfSelection]
try {
val predictor = PassPredictor(tle, gsp.value!!)
val passes = predictor.getPasses(dateNow, hoursAhead, true)
val predictor = PassPredictor(tle, getGSP().value!!)
val passes = predictor.getPasses(dateNow, getHoursAhead(), true)
passes.forEach {
passList.add(SatPass(tle, predictor, it))
}
@ -206,10 +137,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
_debugMessage.postValue(app.getString(R.string.err_sat_wont_pass))
}
}
passList.removeAll { it.pass.startTime.after(dateFuture) }
passList.removeAll { it.pass.endTime.before(dateNow) }
passList.removeAll { it.pass.maxEl < minEl }
passList.sortBy { it.pass.startTime }
passList = filterAndSortList(passList, getHoursAhead())
}
_satPassList.postValue(passList)
} else {
@ -219,42 +147,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
private fun filterAndSortList(
list: MutableList<SatPass>,
hoursAhead: Int
): MutableList<SatPass> {
val dateNow = Date()
val dateFuture = Calendar.getInstance().let {
it.time = dateNow
it.add(Calendar.HOUR, hoursAhead)
it.time
}
list.removeAll { it.pass.startTime.after(dateFuture) }
list.removeAll { it.pass.endTime.before(dateNow) }
list.removeAll { it.pass.maxEl < getMinElevation() }
list.sortBy { it.pass.startTime }
return list
}
suspend fun getTransmittersForSat(id: Int): List<Transmitter> {
return repository.getTransmittersForSat(id)
}
@Suppress("UNCHECKED_CAST")
private fun loadDataFromDisk() {
tleMainList = try {
val tleStream = app.openFileInput(tleMainListFileName)
val tleList = ObjectInputStream(tleStream).readObject()
tleList as List<TLE>
} catch (exception: FileNotFoundException) {
Log.w(tag, app.getString(R.string.err_no_tle_file))
emptyList()
} catch (exception: IOException) {
Log.w(tag, exception.toString())
emptyList()
}
tleSelection = try {
val selectionStream = app.openFileInput(tleSelectionFileName)
val selectionList = ObjectInputStream(selectionStream).readObject()
selectionList as MutableList<Int>
} catch (exception: FileNotFoundException) {
Log.w(tag, app.getString(R.string.err_no_selection_file))
mutableListOf()
} catch (exception: IOException) {
Log.w(tag, exception.toString())
mutableListOf()
}
setGroundStationPosition()
}
}
fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits())
}
fun SharedPreferences.getDouble(key: String, default: Double): Double {
return Double.fromBits(getLong(key, default.toRawBits()))
}

Wyświetl plik

@ -0,0 +1,45 @@
/*
* Look4Sat. Amateur radio and weather satellite tracker and passes predictor for Android.
* Copyright (C) 2019, 2020 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.di
import android.content.Context
import android.content.SharedPreferences
import android.location.LocationManager
import com.rtbishop.look4sat.utility.DataManager
import com.rtbishop.look4sat.utility.PrefsManager
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class UtilityModule {
@Singleton
@Provides
fun providePrefsManager(preferences: SharedPreferences): PrefsManager {
return PrefsManager(preferences)
}
@Singleton
@Provides
fun provideDataManager(locationManager: LocationManager, context: Context): DataManager {
return DataManager(locationManager, context)
}
}

Wyświetl plik

@ -103,7 +103,7 @@ class MainActivity : AppCompatActivity() {
}
private fun setupObservers() {
viewModel.debugMessage.observe(this, Observer { message ->
viewModel.getDebugMessage().observe(this, Observer { message ->
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
when (message) {
getString(R.string.update_loc_success) -> drawerBinding.drawerBtnLoc.isEnabled =
@ -119,7 +119,7 @@ class MainActivity : AppCompatActivity() {
}
})
viewModel.gsp.observe(this, Observer { gsp ->
viewModel.getGSP().observe(this, Observer { gsp ->
drawerBinding.drawerLatValue.text =
String.format(getString(R.string.pat_location), gsp.latitude)
drawerBinding.drawerLonValue.text =

Wyświetl plik

@ -82,9 +82,9 @@ class MapViewFragment : Fragment() {
}
private fun setupComponents() {
val delay = viewModel.delay
gsp = viewModel.gsp.value ?: GroundStationPosition(0.0, 0.0, 0.0)
satPassList = viewModel.satPassList.value ?: emptyList()
val refreshRate = viewModel.getRefreshRate()
gsp = viewModel.getGSP().value ?: GroundStationPosition(0.0, 0.0, 0.0)
satPassList = viewModel.getSatPassList().value ?: emptyList()
if (satPassList.isNotEmpty()) {
satPassList = satPassList.distinctBy { it.tle }
@ -96,8 +96,8 @@ class MapViewFragment : Fragment() {
binding.frameMap.addView(mapView)
service.scheduleAtFixedRate(
{ mapView.invalidate() },
delay,
delay,
refreshRate,
refreshRate,
TimeUnit.MILLISECONDS
)
} else {

Wyświetl plik

@ -97,7 +97,7 @@ class PassListFragment : Fragment() {
binding.refLayoutPassList.setColorSchemeResources(R.color.backgroundDark)
binding.refLayoutPassList.setOnRefreshListener { calculatePasses() }
btnPassPrefs.setOnClickListener {
showSatPassPrefsDialog(viewModel.hoursAhead, viewModel.minEl)
showSatPassPrefsDialog(viewModel.getHoursAhead(), viewModel.getMinElevation())
}
binding.fabSatSelect.setOnClickListener {
showSelectSatDialog(viewModel.tleMainList, viewModel.tleSelection)
@ -105,7 +105,7 @@ class PassListFragment : Fragment() {
}
private fun setupObservers() {
viewModel.satPassList.observe(viewLifecycleOwner, Observer {
viewModel.getSatPassList().observe(viewLifecycleOwner, Observer {
satPassList = it
satPassAdapter.setList(satPassList)
if (satPassList.isNotEmpty()) {

Wyświetl plik

@ -78,8 +78,8 @@ class PolarViewFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.satPassList.value?.let {
val refreshRate = viewModel.delay
viewModel.getSatPassList().value?.let {
val refreshRate = viewModel.getRefreshRate()
satPass = it[args.satPassIndex]
mainActivity.supportActionBar?.title = satPass.tle.name

Wyświetl plik

@ -123,7 +123,7 @@ class SatPassAdapter(val viewModel: MainViewModel) :
binding.passLeoProgress.progress = satPass.progress
itemView.setOnClickListener {
viewModel.satPassList.value?.let {
viewModel.getSatPassList().value?.let {
val satPassIndex = it.indexOf(satPass)
val action = PassListFragmentDirections.actionPassToPolar(satPassIndex)
itemView.findNavController().navigate(action)
@ -148,7 +148,7 @@ class SatPassAdapter(val viewModel: MainViewModel) :
String.format(context.getString(R.string.pat_elevation), satPass.pass.maxEl)
itemView.setOnClickListener {
viewModel.satPassList.value?.let {
viewModel.getSatPassList().value?.let {
val satPassIndex = it.indexOf(satPass)
val action = PassListFragmentDirections.actionPassToPolar(satPassIndex)
itemView.findNavController().navigate(action)

Wyświetl plik

@ -0,0 +1,112 @@
/*
* Look4Sat. Amateur radio and weather satellite tracker and passes predictor for Android.
* Copyright (C) 2019, 2020 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.utility
import android.content.Context
import android.location.LocationManager
import android.util.Log
import com.github.amsacode.predict4java.GroundStationPosition
import com.github.amsacode.predict4java.TLE
import com.rtbishop.look4sat.R
import java.io.FileNotFoundException
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import javax.inject.Inject
class DataManager @Inject constructor(
private val locationManager: LocationManager,
private val context: Context
) {
private val tag = "DataManager"
private val tleMainListFileName = "tleFile.txt"
private val tleSelectionFileName = "tleSelection"
fun getLastKnownLocation(): GroundStationPosition? {
val passiveProvider = LocationManager.PASSIVE_PROVIDER
var groundStationPosition: GroundStationPosition? = null
return try {
val location = locationManager.getLastKnownLocation(passiveProvider)
location?.let {
groundStationPosition =
GroundStationPosition(it.latitude, it.longitude, it.altitude)
}
groundStationPosition
} catch (e: SecurityException) {
null
}
}
fun saveSelectionList(selectionList: MutableList<Int>) {
try {
val fileOutputStream =
context.openFileOutput(tleSelectionFileName, Context.MODE_PRIVATE)
ObjectOutputStream(fileOutputStream).apply {
writeObject(selectionList)
flush()
close()
}
} catch (exception: IOException) {
}
}
fun saveTleList(tleList: MutableList<TLE>) {
try {
val fileOutStream = context.openFileOutput(tleMainListFileName, Context.MODE_PRIVATE)
ObjectOutputStream(fileOutStream).apply {
writeObject(tleList)
flush()
close()
}
} catch (exception: IOException) {
}
}
@Suppress("UNCHECKED_CAST")
fun loadTleList(): List<TLE> {
return try {
val tleStream = context.openFileInput(tleMainListFileName)
val tleList = ObjectInputStream(tleStream).readObject()
tleList as List<TLE>
} catch (exception: FileNotFoundException) {
Log.w(tag, context.getString(R.string.err_no_tle_file))
emptyList()
} catch (exception: IOException) {
Log.w(tag, exception.toString())
emptyList()
}
}
@Suppress("UNCHECKED_CAST")
fun loadSelectionList(): MutableList<Int> {
return try {
val selectionStream = context.openFileInput(tleSelectionFileName)
val selectionList = ObjectInputStream(selectionStream).readObject()
selectionList as MutableList<Int>
} catch (exception: FileNotFoundException) {
Log.w(tag, context.getString(R.string.err_no_selection_file))
mutableListOf()
} catch (exception: IOException) {
Log.w(tag, exception.toString())
mutableListOf()
}
}
}

Wyświetl plik

@ -0,0 +1,92 @@
/*
* Look4Sat. Amateur radio and weather satellite tracker and passes predictor for Android.
* Copyright (C) 2019, 2020 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.utility
import android.content.SharedPreferences
import androidx.core.content.edit
import com.github.amsacode.predict4java.GroundStationPosition
import javax.inject.Inject
class PrefsManager @Inject constructor(private val preferences: SharedPreferences) {
private val keyHoursAhead = "hoursAhead"
private val keyMinElevation = "minEl"
private val keyLatitude = "latitude"
private val keyLongitude = "longitude"
private val keyAltitude = "altitude"
private val keyRefreshRate = "rate"
fun getHoursAhead(): Int {
return preferences.getInt(keyHoursAhead, 8)
}
fun getMinElevation(): Double {
return preferences.getDouble(keyMinElevation, 16.0)
}
fun getRefreshRate(): Long {
return preferences.getString(keyRefreshRate, "3000")!!.toLong()
}
fun getGroundStationPosition(): GroundStationPosition {
val lat = preferences.getString(keyLatitude, "0.0")!!.toDouble()
val lon = preferences.getString(keyLongitude, "0.0")!!.toDouble()
val alt = preferences.getString(keyAltitude, "0.0")!!.toDouble()
return GroundStationPosition(lat, lon, alt)
}
fun setHoursAhead(hours: Int) {
preferences.edit {
putInt(keyHoursAhead, hours)
apply()
}
}
fun setMinElevation(minEl: Double) {
preferences.edit {
putDouble(keyMinElevation, minEl)
apply()
}
}
fun setRefreshRate(rate: Long) {
preferences.edit {
putLong(keyRefreshRate, rate)
apply()
}
}
fun setGroundStationPosition(gsp: GroundStationPosition) {
preferences.edit {
putString(keyLatitude, gsp.latitude.toString())
putString(keyLongitude, gsp.longitude.toString())
putString(keyAltitude, gsp.heightAMSL.toString())
apply()
}
}
private fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits())
}
private fun SharedPreferences.getDouble(key: String, default: Double): Double {
return Double.fromBits(getLong(key, default.toRawBits()))
}
}

Wyświetl plik

@ -26,7 +26,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.0'
classpath 'com.android.tools.build:gradle:3.6.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.2.1"
}