Refactor out TracklistElement, make Track more object-oriented.
I'm sure the reason for TracklistElement was to save memory by not having the waypoints loaded, but the separation of Track / TracklistElement creates lots of back-and-forth conversions and mental overhead. I want to give this a try and see if it actually causes any problems. Moved a lot of helper functions into the classes they operate on.
This commit is contained in:
parent
45de00b9c5
commit
e3bc911de4
12 changed files with 555 additions and 791 deletions
|
@ -28,6 +28,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -36,11 +37,9 @@ import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.y20k.trackbook.core.Track
|
import org.y20k.trackbook.core.Track
|
||||||
import org.y20k.trackbook.core.TracklistElement
|
|
||||||
import org.y20k.trackbook.helpers.*
|
import org.y20k.trackbook.helpers.*
|
||||||
import org.y20k.trackbook.ui.MapFragmentLayoutHolder
|
import org.y20k.trackbook.ui.MapFragmentLayoutHolder
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MapFragment class
|
* MapFragment class
|
||||||
*/
|
*/
|
||||||
|
@ -49,7 +48,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
||||||
/* Define log tag */
|
/* Define log tag */
|
||||||
private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java)
|
private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java)
|
||||||
|
|
||||||
|
|
||||||
/* Main class variables */
|
/* Main class variables */
|
||||||
private var bound: Boolean = false
|
private var bound: Boolean = false
|
||||||
private val handler: Handler = Handler(Looper.getMainLooper())
|
private val handler: Handler = Handler(Looper.getMainLooper())
|
||||||
|
@ -217,7 +215,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
||||||
override fun onMarkerTapped(latitude: Double, longitude: Double) {
|
override fun onMarkerTapped(latitude: Double, longitude: Double) {
|
||||||
super.onMarkerTapped(latitude, longitude)
|
super.onMarkerTapped(latitude, longitude)
|
||||||
if (bound) {
|
if (bound) {
|
||||||
track = TrackHelper.toggleStarred(activity as Context, track, latitude, longitude)
|
TrackHelper.toggle_waypoint_starred(activity as Context, track, latitude, longitude)
|
||||||
layout.overlayCurrentTrack(track, trackingState)
|
layout.overlayCurrentTrack(track, trackingState)
|
||||||
trackerService.track = track
|
trackerService.track = track
|
||||||
}
|
}
|
||||||
|
@ -298,16 +296,12 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CoroutineScope(IO).launch {
|
CoroutineScope(IO).launch {
|
||||||
// step 1: create and store filenames for json and gpx files
|
track.save_json(activity as Context)
|
||||||
track.trackUriString = FileHelper.getTrackFileUri(activity as Context, track).toString()
|
track.save_gpx(activity as Context)
|
||||||
track.gpxUriString = FileHelper.getGpxFileUri(activity as Context, track).toString()
|
|
||||||
// step 2: save track
|
|
||||||
FileHelper.saveTrackSuspended(track, saveGpxToo = true)
|
|
||||||
// step 3: clear track
|
|
||||||
trackerService.clearTrack()
|
trackerService.clearTrack()
|
||||||
// step 4: open track in TrackFragement
|
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
openTrack(track.toTracklistElement(activity as Context))
|
// step 4: open track in TrackFragement
|
||||||
|
openTrack(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,12 +309,12 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
||||||
|
|
||||||
|
|
||||||
/* Opens a track in TrackFragment */
|
/* Opens a track in TrackFragment */
|
||||||
private fun openTrack(tracklistElement: TracklistElement) {
|
private fun openTrack(track: Track) {
|
||||||
val bundle: Bundle = Bundle()
|
val bundle: Bundle = Bundle()
|
||||||
bundle.putString(Keys.ARG_TRACK_TITLE, tracklistElement.name)
|
bundle.putString(Keys.ARG_TRACK_TITLE, track.name)
|
||||||
bundle.putString(Keys.ARG_TRACK_FILE_URI, tracklistElement.trackUriString)
|
bundle.putString(Keys.ARG_TRACK_FILE_URI, track.get_json_file(activity as Context).toUri().toString())
|
||||||
bundle.putString(Keys.ARG_GPX_FILE_URI, tracklistElement.gpxUriString)
|
bundle.putString(Keys.ARG_GPX_FILE_URI, track.get_gpx_file(activity as Context).toUri().toString())
|
||||||
bundle.putLong(Keys.ARG_TRACK_ID, tracklistElement.id)
|
bundle.putLong(Keys.ARG_TRACK_ID, track.id)
|
||||||
findNavController().navigate(R.id.fragment_track, bundle)
|
findNavController().navigate(R.id.fragment_track, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.y20k.trackbook.core.Tracklist
|
import org.y20k.trackbook.core.Tracklist
|
||||||
|
import org.y20k.trackbook.core.load_tracklist
|
||||||
import org.y20k.trackbook.helpers.AppThemeHelper
|
import org.y20k.trackbook.helpers.AppThemeHelper
|
||||||
import org.y20k.trackbook.helpers.FileHelper
|
import org.y20k.trackbook.helpers.FileHelper
|
||||||
import org.y20k.trackbook.helpers.LengthUnitHelper
|
import org.y20k.trackbook.helpers.LengthUnitHelper
|
||||||
|
@ -216,12 +217,8 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||||
|
|
||||||
/* Removes track and track files for given position - used by TracklistFragment */
|
/* Removes track and track files for given position - used by TracklistFragment */
|
||||||
private fun deleteNonStarred(context: Context) {
|
private fun deleteNonStarred(context: Context) {
|
||||||
CoroutineScope(IO).launch {
|
var tracklist: Tracklist = load_tracklist(context)
|
||||||
var tracklist: Tracklist = FileHelper.readTracklist(context)
|
tracklist.delete_non_starred(context)
|
||||||
val deferred: Deferred<Tracklist> = async { FileHelper.deleteNonStarredSuspended(context, tracklist) }
|
|
||||||
// wait for result and store in tracklist
|
|
||||||
tracklist = deferred.await()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -40,7 +41,9 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.y20k.trackbook.core.Track
|
import org.y20k.trackbook.core.Track
|
||||||
|
import org.y20k.trackbook.core.track_from_file
|
||||||
import org.y20k.trackbook.dialogs.RenameTrackDialog
|
import org.y20k.trackbook.dialogs.RenameTrackDialog
|
||||||
|
import org.y20k.trackbook.helpers.DateTimeHelper
|
||||||
import org.y20k.trackbook.helpers.FileHelper
|
import org.y20k.trackbook.helpers.FileHelper
|
||||||
import org.y20k.trackbook.helpers.LogHelper
|
import org.y20k.trackbook.helpers.LogHelper
|
||||||
import org.y20k.trackbook.helpers.MapOverlayHelper
|
import org.y20k.trackbook.helpers.MapOverlayHelper
|
||||||
|
@ -60,7 +63,8 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onCreate from Fragment */
|
/* Overrides onCreate from Fragment */
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?)
|
||||||
|
{
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
trackFileUriString = arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String()
|
trackFileUriString = arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String()
|
||||||
}
|
}
|
||||||
|
@ -70,8 +74,9 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
// initialize layout
|
// initialize layout
|
||||||
val track: Track
|
val track: Track
|
||||||
if (this::trackFileUriString.isInitialized && trackFileUriString.isNotBlank()) {
|
if (this::trackFileUriString.isInitialized && trackFileUriString.isNotBlank())
|
||||||
track = FileHelper.readTrack(activity as Context, Uri.parse(trackFileUriString))
|
{
|
||||||
|
track = track_from_file(activity as Context, Uri.parse(trackFileUriString).toFile())
|
||||||
} else {
|
} else {
|
||||||
track = Track()
|
track = Track()
|
||||||
}
|
}
|
||||||
|
@ -107,13 +112,15 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onResume from Fragment */
|
/* Overrides onResume from Fragment */
|
||||||
override fun onResume() {
|
override fun onResume()
|
||||||
|
{
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onPause from Fragment */
|
/* Overrides onPause from Fragment */
|
||||||
override fun onPause() {
|
override fun onPause()
|
||||||
|
{
|
||||||
super.onPause()
|
super.onPause()
|
||||||
// save zoom level and map center
|
// save zoom level and map center
|
||||||
layout.saveViewStateToTrack()
|
layout.saveViewStateToTrack()
|
||||||
|
@ -125,15 +132,18 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
||||||
|
|
||||||
|
|
||||||
/* Pass the activity result */
|
/* Pass the activity result */
|
||||||
private fun requestSaveGpxResult(result: ActivityResult) {
|
private fun requestSaveGpxResult(result: ActivityResult)
|
||||||
|
{
|
||||||
// save GPX file to result file location
|
// save GPX file to result file location
|
||||||
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
|
if (result.resultCode == Activity.RESULT_OK && result.data != null)
|
||||||
val sourceUri: Uri = Uri.parse(layout.track.gpxUriString)
|
{
|
||||||
|
val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri()
|
||||||
val targetUri: Uri? = result.data?.data
|
val targetUri: Uri? = result.data?.data
|
||||||
if (targetUri != null) {
|
if (targetUri != null)
|
||||||
|
{
|
||||||
// copy file async (= fire & forget - no return value needed)
|
// copy file async (= fire & forget - no return value needed)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri)
|
FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri)
|
||||||
}
|
}
|
||||||
Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show()
|
Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
@ -142,7 +152,8 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onRenameTrackDialog from RenameTrackDialog */
|
/* Overrides onRenameTrackDialog from RenameTrackDialog */
|
||||||
override fun onRenameTrackDialog(textInput: String) {
|
override fun onRenameTrackDialog(textInput: String)
|
||||||
|
{
|
||||||
// rename track async (= fire & forget - no return value needed)
|
// rename track async (= fire & forget - no return value needed)
|
||||||
CoroutineScope(Dispatchers.IO).launch { FileHelper.renameTrackSuspended(activity as Context, layout.track, textInput) }
|
CoroutineScope(Dispatchers.IO).launch { FileHelper.renameTrackSuspended(activity as Context, layout.track, textInput) }
|
||||||
// update name in layout
|
// update name in layout
|
||||||
|
@ -152,10 +163,13 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onYesNoDialog from YesNoDialogListener */
|
/* Overrides onYesNoDialog from YesNoDialogListener */
|
||||||
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
|
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String)
|
||||||
when (type) {
|
{
|
||||||
|
when (type)
|
||||||
|
{
|
||||||
Keys.DIALOG_DELETE_TRACK -> {
|
Keys.DIALOG_DELETE_TRACK -> {
|
||||||
when (dialogResult) {
|
when (dialogResult)
|
||||||
|
{
|
||||||
// user tapped remove track
|
// user tapped remove track
|
||||||
true -> {
|
true -> {
|
||||||
// switch to TracklistFragment and remove track there
|
// switch to TracklistFragment and remove track there
|
||||||
|
@ -169,25 +183,31 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onMarkerTapped from MarkerListener */
|
/* Overrides onMarkerTapped from MarkerListener */
|
||||||
override fun onMarkerTapped(latitude: Double, longitude: Double) {
|
override fun onMarkerTapped(latitude: Double, longitude: Double)
|
||||||
|
{
|
||||||
super.onMarkerTapped(latitude, longitude)
|
super.onMarkerTapped(latitude, longitude)
|
||||||
// update track display
|
TrackHelper.toggle_waypoint_starred(activity as Context, layout.track, latitude, longitude)
|
||||||
layout.track = TrackHelper.toggleStarred(activity as Context, layout.track, latitude, longitude)
|
|
||||||
layout.updateTrackOverlay()
|
layout.updateTrackOverlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Opens up a file picker to select the save location */
|
/* Opens up a file picker to select the save location */
|
||||||
private fun openSaveGpxDialog() {
|
private fun openSaveGpxDialog()
|
||||||
|
{
|
||||||
|
val context = this.activity as Context
|
||||||
|
val export_name: String = DateTimeHelper.convertToSortableDateString(layout.track.recordingStart) + Keys.GPX_FILE_EXTENSION
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = Keys.MIME_TYPE_GPX
|
type = Keys.MIME_TYPE_GPX
|
||||||
putExtra(Intent.EXTRA_TITLE, FileHelper.getGpxFileName(layout.track))
|
putExtra(Intent.EXTRA_TITLE, export_name)
|
||||||
}
|
}
|
||||||
// file gets saved in the ActivityResult
|
// file gets saved in the ActivityResult
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
requestSaveGpxLauncher.launch(intent)
|
requestSaveGpxLauncher.launch(intent)
|
||||||
} catch (e: Exception) {
|
}
|
||||||
|
catch (e: Exception)
|
||||||
|
{
|
||||||
LogHelper.e(TAG, "Unable to save GPX.")
|
LogHelper.e(TAG, "Unable to save GPX.")
|
||||||
Toast.makeText(activity as Context, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
|
Toast.makeText(activity as Context, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
@ -195,8 +215,9 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
||||||
|
|
||||||
|
|
||||||
/* Share track as GPX via share sheet */
|
/* Share track as GPX via share sheet */
|
||||||
private fun shareGpxTrack() {
|
private fun shareGpxTrack()
|
||||||
val gpxFile = Uri.parse(layout.track.gpxUriString).toFile()
|
{
|
||||||
|
val gpxFile = layout.track.get_gpx_file(this.activity as Context)
|
||||||
val gpxShareUri = FileProvider.getUriForFile(this.activity as Context, "${requireActivity().applicationContext.packageName}.provider", gpxFile)
|
val gpxShareUri = FileProvider.getUriForFile(this.activity as Context, "${requireActivity().applicationContext.packageName}.provider", gpxFile)
|
||||||
val shareIntent: Intent = Intent.createChooser(Intent().apply {
|
val shareIntent: Intent = Intent.createChooser(Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
|
@ -208,9 +229,12 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
||||||
|
|
||||||
// show share sheet - if file helper is available
|
// show share sheet - if file helper is available
|
||||||
val packageManager: PackageManager? = activity?.packageManager
|
val packageManager: PackageManager? = activity?.packageManager
|
||||||
if (packageManager != null && shareIntent.resolveActivity(packageManager) != null) {
|
if (packageManager != null && shareIntent.resolveActivity(packageManager) != null)
|
||||||
|
{
|
||||||
startActivity(shareIntent)
|
startActivity(shareIntent)
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
Toast.makeText(activity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
|
Toast.makeText(activity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.Runnable
|
import kotlinx.coroutines.Runnable
|
||||||
import org.y20k.trackbook.core.Track
|
import org.y20k.trackbook.core.Track
|
||||||
|
import org.y20k.trackbook.core.load_temp_track
|
||||||
import org.y20k.trackbook.helpers.*
|
import org.y20k.trackbook.helpers.*
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -61,6 +62,8 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
var currentBestLocation: Location = LocationHelper.getDefaultLocation()
|
var currentBestLocation: Location = LocationHelper.getDefaultLocation()
|
||||||
var lastSave: Date = Keys.DEFAULT_DATE
|
var lastSave: Date = Keys.DEFAULT_DATE
|
||||||
var stepCountOffset: Float = 0f
|
var stepCountOffset: Float = 0f
|
||||||
|
// The resumed flag will be true for the first point that is received after unpausing a
|
||||||
|
// recording, so that the distance travelled while paused is not added to the track.distance.
|
||||||
var resumed: Boolean = false
|
var resumed: Boolean = false
|
||||||
var track: Track = Track()
|
var track: Track = Track()
|
||||||
var gpsLocationListenerRegistered: Boolean = false
|
var gpsLocationListenerRegistered: Boolean = false
|
||||||
|
@ -93,7 +96,7 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
networkLocationListener = createLocationListener()
|
networkLocationListener = createLocationListener()
|
||||||
trackingState = PreferencesHelper.loadTrackingState()
|
trackingState = PreferencesHelper.loadTrackingState()
|
||||||
currentBestLocation = LocationHelper.getLastKnownLocation(this)
|
currentBestLocation = LocationHelper.getLastKnownLocation(this)
|
||||||
track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this))
|
track = load_temp_track(this)
|
||||||
// altitudeValues.capacity = PreferencesHelper.loadAltitudeSmoothingValue()
|
// altitudeValues.capacity = PreferencesHelper.loadAltitudeSmoothingValue()
|
||||||
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
|
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
|
||||||
}
|
}
|
||||||
|
@ -195,72 +198,68 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
|
|
||||||
/* Resume tracking after stop/pause */
|
/* Resume tracking after stop/pause */
|
||||||
fun resumeTracking() {
|
fun resumeTracking() {
|
||||||
// load temp track - returns an empty track if not available
|
// load temp track - returns an empty track if there is no temp file.
|
||||||
track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this))
|
track = load_temp_track(this)
|
||||||
// try to mark last waypoint as stopover
|
// try to mark last waypoint as stopover
|
||||||
if (track.wayPoints.size > 0) {
|
if (track.wayPoints.size > 0) {
|
||||||
val lastWayPointIndex = track.wayPoints.size - 1
|
val lastWayPointIndex = track.wayPoints.size - 1
|
||||||
track.wayPoints[lastWayPointIndex].isStopOver = true
|
track.wayPoints[lastWayPointIndex].isStopOver = true
|
||||||
}
|
}
|
||||||
// set resumed flag
|
|
||||||
resumed = true
|
resumed = true
|
||||||
// calculate length of recording break
|
// calculate length of recording break
|
||||||
track.recordingPaused = track.recordingPaused + TrackHelper.calculateDurationOfPause(track.recordingStop)
|
track.recordingPaused += TrackHelper.calculateDurationOfPause(track.recordingStop)
|
||||||
// start tracking
|
|
||||||
startTracking(newTrack = false)
|
startTracking(newTrack = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Start tracking location */
|
/* Start tracking location */
|
||||||
fun startTracking(newTrack: Boolean = true) {
|
fun startTracking(newTrack: Boolean = true) {
|
||||||
// start receiving location updates
|
|
||||||
addGpsLocationListener()
|
addGpsLocationListener()
|
||||||
addNetworkLocationListener()
|
addNetworkLocationListener()
|
||||||
// set up new track
|
// set up new track
|
||||||
if (newTrack) {
|
if (newTrack) {
|
||||||
track = Track()
|
track = Track()
|
||||||
track.name = DateTimeHelper.convertToReadableDate(track.recordingStart)
|
resumed = false
|
||||||
stepCountOffset = 0f
|
stepCountOffset = 0f
|
||||||
}
|
}
|
||||||
// set state
|
|
||||||
trackingState = Keys.STATE_TRACKING_ACTIVE
|
trackingState = Keys.STATE_TRACKING_ACTIVE
|
||||||
PreferencesHelper.saveTrackingState(trackingState)
|
PreferencesHelper.saveTrackingState(trackingState)
|
||||||
// start recording steps and location fixes
|
|
||||||
startStepCounter()
|
startStepCounter()
|
||||||
handler.postDelayed(periodicTrackUpdate, 0)
|
handler.postDelayed(periodicTrackUpdate, 0)
|
||||||
// show notification
|
|
||||||
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
|
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stop tracking location */
|
/* Stop tracking location */
|
||||||
fun stopTracking() {
|
fun stopTracking() {
|
||||||
// save temp track
|
|
||||||
track.recordingStop = GregorianCalendar.getInstance().time
|
track.recordingStop = GregorianCalendar.getInstance().time
|
||||||
CoroutineScope(IO).launch { FileHelper.saveTempTrackSuspended(this@TrackerService, track) }
|
val context: Context = this as Context
|
||||||
// save state
|
CoroutineScope(IO).launch { track.save_temp_suspended(context) }
|
||||||
|
|
||||||
|
trackingState = Keys.STATE_TRACKING_STOPPED
|
||||||
trackingState = Keys.STATE_TRACKING_PAUSED
|
trackingState = Keys.STATE_TRACKING_PAUSED
|
||||||
PreferencesHelper.saveTrackingState(trackingState)
|
PreferencesHelper.saveTrackingState(trackingState)
|
||||||
// reset altitude values queue
|
|
||||||
altitudeValues.reset()
|
altitudeValues.reset()
|
||||||
// stop recording steps and location fixes
|
|
||||||
sensorManager.unregisterListener(this)
|
sensorManager.unregisterListener(this)
|
||||||
handler.removeCallbacks(periodicTrackUpdate)
|
handler.removeCallbacks(periodicTrackUpdate)
|
||||||
// update notification
|
|
||||||
displayNotification()
|
displayNotification()
|
||||||
stopForeground(false)
|
stopForeground(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clear track recording */
|
fun clearTrack()
|
||||||
fun clearTrack() {
|
{
|
||||||
track = Track()
|
track = Track()
|
||||||
FileHelper.deleteTempFile(this)
|
resumed = false
|
||||||
|
FileHelper.delete_temp_file(this as Context)
|
||||||
trackingState = Keys.STATE_TRACKING_NOT_STARTED
|
trackingState = Keys.STATE_TRACKING_NOT_STARTED
|
||||||
PreferencesHelper.saveTrackingState(trackingState)
|
PreferencesHelper.saveTrackingState(trackingState)
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
notificationManager.cancel(Keys.TRACKER_SERVICE_NOTIFICATION_ID) // this call was not necessary prior to Android 12
|
notificationManager.cancel(Keys.TRACKER_SERVICE_NOTIFICATION_ID) // this call was not necessary prior to Android 12
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Creates location listener */
|
private fun createLocationListener(): LocationListener
|
||||||
private fun createLocationListener(): LocationListener {
|
{
|
||||||
return object : LocationListener {
|
return object : LocationListener {
|
||||||
override fun onLocationChanged(location: Location) {
|
override fun onLocationChanged(location: Location) {
|
||||||
// update currentBestLocation if a better location is available
|
// update currentBestLocation if a better location is available
|
||||||
|
@ -268,7 +267,8 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
currentBestLocation = location
|
currentBestLocation = location
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onProviderEnabled(provider: String) {
|
override fun onProviderEnabled(provider: String)
|
||||||
|
{
|
||||||
LogHelper.v(TAG, "onProviderEnabled $provider")
|
LogHelper.v(TAG, "onProviderEnabled $provider")
|
||||||
when (provider) {
|
when (provider) {
|
||||||
LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(
|
LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(
|
||||||
|
@ -280,7 +280,8 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onProviderDisabled(provider: String) {
|
override fun onProviderDisabled(provider: String)
|
||||||
|
{
|
||||||
LogHelper.v(TAG, "onProviderDisabled $provider")
|
LogHelper.v(TAG, "onProviderDisabled $provider")
|
||||||
when (provider) {
|
when (provider) {
|
||||||
LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(
|
LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(
|
||||||
|
@ -292,59 +293,58 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) {
|
override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?)
|
||||||
|
{
|
||||||
// deprecated method
|
// deprecated method
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adds a GPS location listener to location manager */
|
/* Adds a GPS location listener to location manager */
|
||||||
private fun addGpsLocationListener() {
|
private fun addGpsLocationListener()
|
||||||
// check if already registered
|
{
|
||||||
if (!gpsLocationListenerRegistered) {
|
if (gpsLocationListenerRegistered)
|
||||||
// check if Network provider is available
|
{
|
||||||
gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
|
LogHelper.v(TAG, "GPS location listener has already been added.")
|
||||||
if (gpsProviderActive) {
|
return
|
||||||
// check for location permission
|
|
||||||
if (ContextCompat.checkSelfPermission(
|
|
||||||
this,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
// adds GPS location listener
|
|
||||||
locationManager.requestLocationUpdates(
|
|
||||||
LocationManager.GPS_PROVIDER,
|
|
||||||
0,
|
|
||||||
0f,
|
|
||||||
gpsLocationListener
|
|
||||||
)
|
|
||||||
gpsLocationListenerRegistered = true
|
|
||||||
LogHelper.v(TAG, "Added GPS location listener.")
|
|
||||||
} else {
|
|
||||||
LogHelper.w(
|
|
||||||
TAG,
|
|
||||||
"Unable to add GPS location listener. Location permission is not granted."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogHelper.w(TAG, "Unable to add GPS location listener.")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogHelper.v(TAG, "Skipping registration. GPS location listener has already been added.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
|
||||||
|
if (! gpsProviderActive)
|
||||||
|
{
|
||||||
|
LogHelper.w(TAG, "Device GPS is not enabled.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val has_permission: Boolean = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
if (! has_permission)
|
||||||
|
{
|
||||||
|
LogHelper.w(TAG, "Location permission is not granted.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
locationManager.requestLocationUpdates(
|
||||||
|
LocationManager.GPS_PROVIDER,
|
||||||
|
0,
|
||||||
|
0f,
|
||||||
|
gpsLocationListener
|
||||||
|
)
|
||||||
|
gpsLocationListenerRegistered = true
|
||||||
|
LogHelper.v(TAG, "Added GPS location listener.")
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adds a Network location listener to location manager */
|
/* Adds a Network location listener to location manager */
|
||||||
private fun addNetworkLocationListener() {
|
private fun addNetworkLocationListener() {
|
||||||
if (gpsOnly)
|
if (gpsOnly)
|
||||||
{
|
{
|
||||||
LogHelper.v(TAG, "User prefers GPS-only.")
|
LogHelper.v(TAG, "Skipping Network listener. User prefers GPS-only.")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (networkLocationListenerRegistered)
|
if (networkLocationListenerRegistered)
|
||||||
{
|
{
|
||||||
LogHelper.v(TAG, "Network location listener has already been added.")
|
LogHelper.v(TAG, "Network location listener has already been added.")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
|
networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
|
||||||
|
@ -378,10 +378,7 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
gpsLocationListenerRegistered = false
|
gpsLocationListenerRegistered = false
|
||||||
LogHelper.v(TAG, "Removed GPS location listener.")
|
LogHelper.v(TAG, "Removed GPS location listener.")
|
||||||
} else {
|
} else {
|
||||||
LogHelper.w(
|
LogHelper.w(TAG, "Unable to remove GPS location listener. Location permission is needed.")
|
||||||
TAG,
|
|
||||||
"Unable to remove GPS location listener. Location permission is needed."
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,10 +389,7 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
networkLocationListenerRegistered = false
|
networkLocationListenerRegistered = false
|
||||||
LogHelper.v(TAG, "Removed Network location listener.")
|
LogHelper.v(TAG, "Removed Network location listener.")
|
||||||
} else {
|
} else {
|
||||||
LogHelper.w(
|
LogHelper.w(TAG, "Unable to remove Network location listener. Location permission is needed.")
|
||||||
TAG,
|
|
||||||
"Unable to remove Network location listener. Location permission is needed."
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -424,25 +418,25 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
* Defines the listener for changes in shared preferences
|
* Defines the listener for changes in shared preferences
|
||||||
*/
|
*/
|
||||||
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||||
when (key) {
|
when (key) {
|
||||||
// preference "Restrict to GPS"
|
// preference "Restrict to GPS"
|
||||||
Keys.PREF_GPS_ONLY -> {
|
Keys.PREF_GPS_ONLY -> {
|
||||||
gpsOnly = PreferencesHelper.loadGpsOnly()
|
gpsOnly = PreferencesHelper.loadGpsOnly()
|
||||||
when (gpsOnly) {
|
when (gpsOnly) {
|
||||||
true -> removeNetworkLocationListener()
|
true -> removeNetworkLocationListener()
|
||||||
false -> addNetworkLocationListener()
|
false -> addNetworkLocationListener()
|
||||||
}
|
|
||||||
}
|
|
||||||
// preference "Use Imperial Measurements"
|
|
||||||
Keys.PREF_USE_IMPERIAL_UNITS -> {
|
|
||||||
useImperial = PreferencesHelper.loadUseImperialUnits()
|
|
||||||
}
|
|
||||||
// preference "Recording Accuracy"
|
|
||||||
Keys.PREF_OMIT_RESTS -> {
|
|
||||||
omitRests = PreferencesHelper.loadOmitRests()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// preference "Use Imperial Measurements"
|
||||||
|
Keys.PREF_USE_IMPERIAL_UNITS -> {
|
||||||
|
useImperial = PreferencesHelper.loadUseImperialUnits()
|
||||||
|
}
|
||||||
|
// preference "Recording Accuracy"
|
||||||
|
Keys.PREF_OMIT_RESTS -> {
|
||||||
|
omitRests = PreferencesHelper.loadOmitRests()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
* End of declaration
|
* End of declaration
|
||||||
*/
|
*/
|
||||||
|
@ -460,19 +454,13 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
/*
|
/*
|
||||||
* Runnable: Periodically track updates (if recording active)
|
* Runnable: Periodically track updates (if recording active)
|
||||||
*/
|
*/
|
||||||
private val periodicTrackUpdate: Runnable = object : Runnable {
|
private val periodicTrackUpdate: Runnable = object : Runnable
|
||||||
|
{
|
||||||
override fun run() {
|
override fun run() {
|
||||||
// add waypoint to track - step count is continuously updated in onSensorChanged
|
// add waypoint to track - step count is continuously updated in onSensorChanged
|
||||||
val result: Pair<Boolean, Track> = TrackHelper.addWayPointToTrack(track, currentBestLocation, omitRests, resumed)
|
val success = track.add_waypoint(currentBestLocation, omitRests, resumed)
|
||||||
// get results
|
if (success) {
|
||||||
val successfullyAdded: Boolean = result.first
|
resumed = false
|
||||||
track = result.second
|
|
||||||
// check, if waypoint was added
|
|
||||||
if (successfullyAdded) {
|
|
||||||
// reset resumed flag, if necessary
|
|
||||||
if (resumed) {
|
|
||||||
resumed = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// store previous smoothed altitude
|
// store previous smoothed altitude
|
||||||
val previousAltitude: Double = altitudeValues.getAverage()
|
val previousAltitude: Double = altitudeValues.getAverage()
|
||||||
|
@ -480,7 +468,7 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
val currentBestLocationAltitude: Double = currentBestLocation.altitude
|
val currentBestLocationAltitude: Double = currentBestLocation.altitude
|
||||||
if (currentBestLocationAltitude != Keys.DEFAULT_ALTITUDE) altitudeValues.add(currentBestLocationAltitude)
|
if (currentBestLocationAltitude != Keys.DEFAULT_ALTITUDE) altitudeValues.add(currentBestLocationAltitude)
|
||||||
// TODO remove
|
// TODO remove
|
||||||
// uncomment to use test altitude values - useful if testing wirth an emulator
|
// uncomment to use test altitude values - useful if testing with an emulator
|
||||||
//altitudeValues.add(getTestAltitude()) // TODO remove
|
//altitudeValues.add(getTestAltitude()) // TODO remove
|
||||||
// TODO remove
|
// TODO remove
|
||||||
|
|
||||||
|
@ -499,7 +487,7 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
val now: Date = GregorianCalendar.getInstance().time
|
val now: Date = GregorianCalendar.getInstance().time
|
||||||
if (now.time - lastSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) {
|
if (now.time - lastSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) {
|
||||||
lastSave = now
|
lastSave = now
|
||||||
CoroutineScope(IO).launch { FileHelper.saveTempTrackSuspended(this@TrackerService, track) }
|
CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// update notification
|
// update notification
|
||||||
|
@ -514,10 +502,12 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
|
|
||||||
/* Simple queue that evicts older elements and holds an average */
|
/* Simple queue that evicts older elements and holds an average */
|
||||||
/* Credit: CircularQueue https://stackoverflow.com/a/51923797 */
|
/* Credit: CircularQueue https://stackoverflow.com/a/51923797 */
|
||||||
class SimpleMovingAverageQueue(var capacity: Int) : LinkedList<Double>() {
|
class SimpleMovingAverageQueue(var capacity: Int) : LinkedList<Double>()
|
||||||
|
{
|
||||||
var prepared: Boolean = false
|
var prepared: Boolean = false
|
||||||
private var sum: Double = 0.0
|
private var sum: Double = 0.0
|
||||||
override fun add(element: Double): Boolean {
|
override fun add(element: Double): Boolean
|
||||||
|
{
|
||||||
prepared = this.size + 1 >= Keys.MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION
|
prepared = this.size + 1 >= Keys.MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION
|
||||||
if (this.size >= capacity) {
|
if (this.size >= capacity) {
|
||||||
sum -= this.first
|
sum -= this.first
|
||||||
|
@ -526,8 +516,12 @@ class TrackerService: Service(), SensorEventListener {
|
||||||
sum += element
|
sum += element
|
||||||
return super.add(element)
|
return super.add(element)
|
||||||
}
|
}
|
||||||
fun getAverage(): Double = sum / this.size
|
fun getAverage(): Double
|
||||||
fun reset() {
|
{
|
||||||
|
return sum / this.size
|
||||||
|
}
|
||||||
|
fun reset()
|
||||||
|
{
|
||||||
this.clear()
|
this.clear()
|
||||||
prepared = false
|
prepared = false
|
||||||
sum = 0.0
|
sum = 0.0
|
||||||
|
|
|
@ -24,6 +24,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -33,11 +34,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import org.y20k.trackbook.core.Tracklist
|
import org.y20k.trackbook.core.Track
|
||||||
import org.y20k.trackbook.core.TracklistElement
|
|
||||||
import org.y20k.trackbook.helpers.FileHelper
|
|
||||||
import org.y20k.trackbook.helpers.LogHelper
|
import org.y20k.trackbook.helpers.LogHelper
|
||||||
import org.y20k.trackbook.helpers.TrackHelper
|
|
||||||
import org.y20k.trackbook.helpers.UiHelper
|
import org.y20k.trackbook.helpers.UiHelper
|
||||||
import org.y20k.trackbook.tracklist.TracklistAdapter
|
import org.y20k.trackbook.tracklist.TracklistAdapter
|
||||||
|
|
||||||
|
@ -95,19 +93,17 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
|
||||||
return rootView
|
return rootView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onTrackElementTapped from TracklistElementAdapterListener */
|
/* Overrides onTrackElementTapped from TracklistElementAdapterListener */
|
||||||
override fun onTrackElementTapped(tracklistElement: TracklistElement) {
|
override fun onTrackElementTapped(track: Track) {
|
||||||
val bundle: Bundle = bundleOf(
|
val bundle: Bundle = bundleOf(
|
||||||
Keys.ARG_TRACK_TITLE to tracklistElement.name,
|
Keys.ARG_TRACK_TITLE to track.name,
|
||||||
Keys.ARG_TRACK_FILE_URI to tracklistElement.trackUriString,
|
Keys.ARG_TRACK_FILE_URI to track.get_json_file(activity as Context).toUri().toString(),
|
||||||
Keys.ARG_GPX_FILE_URI to tracklistElement.gpxUriString,
|
Keys.ARG_GPX_FILE_URI to track.get_gpx_file(activity as Context).toUri().toString(),
|
||||||
Keys.ARG_TRACK_ID to tracklistElement.id
|
Keys.ARG_TRACK_ID to track.id
|
||||||
)
|
)
|
||||||
findNavController().navigate(R.id.fragment_track, bundle)
|
findNavController().navigate(R.id.fragment_track, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onYesNoDialog from YesNoDialogListener */
|
/* Overrides onYesNoDialog from YesNoDialogListener */
|
||||||
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
|
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
@ -117,7 +113,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
|
||||||
// user tapped remove track
|
// user tapped remove track
|
||||||
true -> {
|
true -> {
|
||||||
toggleOnboardingLayout()
|
toggleOnboardingLayout()
|
||||||
val deferred: Deferred<Unit> = async { tracklistAdapter.removeTrackAtPositionSuspended(activity as Context, payload) }
|
val deferred: Deferred<Unit> = async { tracklistAdapter.delete_track_at_position_suspended(activity as Context, payload) }
|
||||||
// wait for result and store in tracklist
|
// wait for result and store in tracklist
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
deferred.await()
|
deferred.await()
|
||||||
|
@ -173,7 +169,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CoroutineScope(Main). launch {
|
CoroutineScope(Main). launch {
|
||||||
tracklistAdapter.removeTrackById(this@TracklistFragment.activity as Context, deleteTrackId)
|
tracklistAdapter.delete_track_by_id(this@TracklistFragment.activity as Context, deleteTrackId)
|
||||||
toggleOnboardingLayout()
|
toggleOnboardingLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,56 +17,280 @@
|
||||||
package org.y20k.trackbook.core
|
package org.y20k.trackbook.core
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.location.Location
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import com.google.gson.annotations.Expose
|
import com.google.gson.annotations.Expose
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.y20k.trackbook.Keys
|
import org.y20k.trackbook.Keys
|
||||||
import org.y20k.trackbook.helpers.DateTimeHelper
|
import org.y20k.trackbook.helpers.DateTimeHelper
|
||||||
|
import org.y20k.trackbook.helpers.FileHelper
|
||||||
|
import org.y20k.trackbook.helpers.LocationHelper
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Track data class
|
* Track data class
|
||||||
*/
|
*/
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Track (@Expose val id: Long = make_random_id(),
|
data class Track (
|
||||||
@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
|
@Expose val id: Long = make_random_id(),
|
||||||
@Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(),
|
@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
|
||||||
@Expose var distance: Float = 0f,
|
@Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(),
|
||||||
@Expose var duration: Long = 0L,
|
@Expose var distance: Float = 0f,
|
||||||
@Expose var recordingPaused: Long = 0L,
|
@Expose var duration: Long = 0L,
|
||||||
@Expose var stepCount: Float = 0f,
|
@Expose var recordingPaused: Long = 0L,
|
||||||
@Expose var recordingStart: Date = GregorianCalendar.getInstance().time,
|
@Expose var stepCount: Float = 0f,
|
||||||
@Expose var recordingStop: Date = recordingStart,
|
@Expose var recordingStart: Date = GregorianCalendar.getInstance().time,
|
||||||
@Expose var maxAltitude: Double = 0.0,
|
@Expose var dateString: String = DateTimeHelper.convertToReadableDate(recordingStart),
|
||||||
@Expose var minAltitude: Double = 0.0,
|
@Expose var recordingStop: Date = recordingStart,
|
||||||
@Expose var positiveElevation: Double = 0.0,
|
@Expose var maxAltitude: Double = 0.0,
|
||||||
@Expose var negativeElevation: Double = 0.0,
|
@Expose var minAltitude: Double = 0.0,
|
||||||
@Expose var trackUriString: String = String(),
|
@Expose var positiveElevation: Double = 0.0,
|
||||||
@Expose var gpxUriString: String = String(),
|
@Expose var negativeElevation: Double = 0.0,
|
||||||
@Expose var latitude: Double = Keys.DEFAULT_LATITUDE,
|
@Expose var latitude: Double = Keys.DEFAULT_LATITUDE,
|
||||||
@Expose var longitude: Double = Keys.DEFAULT_LONGITUDE,
|
@Expose var longitude: Double = Keys.DEFAULT_LONGITUDE,
|
||||||
@Expose var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL,
|
@Expose var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL,
|
||||||
@Expose var name: String = String()): Parcelable
|
@Expose var name: String = DateTimeHelper.convertToReadableDate(recordingStart),
|
||||||
|
@Expose var starred: Boolean = false,
|
||||||
|
): Parcelable
|
||||||
{
|
{
|
||||||
/* Creates a TracklistElement */
|
fun add_waypoint(location: Location, omitRests: Boolean, resumed: Boolean): Boolean
|
||||||
fun toTracklistElement(context: Context): TracklistElement {
|
{
|
||||||
val readableDateString: String = DateTimeHelper.convertToReadableDate(recordingStart)
|
// Step 1: Get previous location
|
||||||
val readableDurationString: String = DateTimeHelper.convertToReadableTime(context, duration)
|
val previousLocation: Location?
|
||||||
return TracklistElement(
|
var numberOfWayPoints: Int = this.wayPoints.size
|
||||||
id = id,
|
|
||||||
name = name,
|
// CASE: First location
|
||||||
date = recordingStart,
|
if (numberOfWayPoints == 0)
|
||||||
dateString = readableDateString,
|
{
|
||||||
distance = distance,
|
previousLocation = null
|
||||||
duration = duration,
|
}
|
||||||
trackUriString = trackUriString,
|
// CASE: Second location - check if first location was plausible & remove implausible location
|
||||||
gpxUriString = gpxUriString,
|
else if (numberOfWayPoints == 1 && !LocationHelper.isFirstLocationPlausible(location, this))
|
||||||
starred = false
|
{
|
||||||
|
previousLocation = null
|
||||||
|
numberOfWayPoints = 0
|
||||||
|
this.wayPoints.removeAt(0)
|
||||||
|
}
|
||||||
|
// CASE: Third location or second location (if first was plausible)
|
||||||
|
else
|
||||||
|
{
|
||||||
|
previousLocation = this.wayPoints[numberOfWayPoints - 1].toLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Update duration
|
||||||
|
val now: Date = GregorianCalendar.getInstance().time
|
||||||
|
val difference: Long = now.time - this.recordingStop.time
|
||||||
|
this.duration += difference
|
||||||
|
this.recordingStop = now
|
||||||
|
|
||||||
|
// Step 3: Add waypoint, if recent and accurate and different enough
|
||||||
|
val shouldBeAdded: Boolean = (
|
||||||
|
LocationHelper.isRecentEnough(location) &&
|
||||||
|
LocationHelper.isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY) &&
|
||||||
|
LocationHelper.isDifferentEnough(previousLocation, location, omitRests)
|
||||||
)
|
)
|
||||||
|
if (! shouldBeAdded)
|
||||||
|
{
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Step 3.1: Update distance (do not update if resumed -> we do not want to add values calculated during a recording pause)
|
||||||
|
if (!resumed)
|
||||||
|
{
|
||||||
|
this.distance = this.distance + LocationHelper.calculateDistance(previousLocation, location)
|
||||||
|
}
|
||||||
|
// Step 3.2: Update altitude values
|
||||||
|
val altitude: Double = location.altitude
|
||||||
|
if (altitude != 0.0)
|
||||||
|
{
|
||||||
|
if (numberOfWayPoints == 0)
|
||||||
|
{
|
||||||
|
this.maxAltitude = altitude
|
||||||
|
this.minAltitude = altitude
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (altitude > this.maxAltitude) this.maxAltitude = altitude
|
||||||
|
if (altitude < this.minAltitude) this.minAltitude = altitude
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Step 3.3: Toggle stop over status, if necessary
|
||||||
|
if (this.wayPoints.size < 0)
|
||||||
|
{
|
||||||
|
this.wayPoints[this.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3.4: Add current location as point to center on for later display
|
||||||
|
this.latitude = location.latitude
|
||||||
|
this.longitude = location.longitude
|
||||||
|
|
||||||
|
// Step 3.5: Add location as new waypoint
|
||||||
|
this.wayPoints.add(WayPoint(location = location, distanceToStartingPoint = this.distance))
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun delete(context: Context)
|
||||||
|
{
|
||||||
|
Log.i("VOUSSOIR", "Deleting track ${this.id}.")
|
||||||
|
val json_file: File = this.get_json_file(context)
|
||||||
|
if (json_file.isFile)
|
||||||
|
{
|
||||||
|
json_file.delete()
|
||||||
|
}
|
||||||
|
val gpx_file: File = this.get_gpx_file(context)
|
||||||
|
if (gpx_file.isFile)
|
||||||
|
{
|
||||||
|
gpx_file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete_suspended(context: Context)
|
||||||
|
{
|
||||||
|
return suspendCoroutine { cont ->
|
||||||
|
cont.resume(this.delete(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get_gpx_file(context: Context): File
|
||||||
|
{
|
||||||
|
val basename: String = this.id.toString() + Keys.GPX_FILE_EXTENSION
|
||||||
|
return File(context.getExternalFilesDir(Keys.FOLDER_GPX), basename)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get_json_file(context: Context): File
|
||||||
|
{
|
||||||
|
val basename: String = this.id.toString() + Keys.TRACKBOOK_FILE_EXTENSION
|
||||||
|
return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), basename)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save_both(context: Context)
|
||||||
|
{
|
||||||
|
this.save_json(context)
|
||||||
|
this.save_gpx(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun save_both_suspended(context: Context)
|
||||||
|
{
|
||||||
|
return suspendCoroutine { cont ->
|
||||||
|
cont.resume(this.save_both(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save_gpx(context: Context)
|
||||||
|
{
|
||||||
|
val gpx: String = this.to_gpx()
|
||||||
|
FileHelper.write_text_file_noblank(gpx, this.get_gpx_file(context))
|
||||||
|
Log.i("VOUSSOIR", "Saved ${this.id}.gpx")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun save_gpx_suspended(context: Context)
|
||||||
|
{
|
||||||
|
return suspendCoroutine { cont ->
|
||||||
|
cont.resume(this.save_gpx(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save_json(context: Context)
|
||||||
|
{
|
||||||
|
val json: String = this.to_json()
|
||||||
|
FileHelper.write_text_file_noblank(json, this.get_json_file(context))
|
||||||
|
Log.i("VOUSSOIR", "Saved ${this.id}.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun save_json_suspended(context: Context)
|
||||||
|
{
|
||||||
|
return suspendCoroutine { cont ->
|
||||||
|
cont.resume(this.save_json(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save_temp(context: Context)
|
||||||
|
{
|
||||||
|
val json: String = this.to_json()
|
||||||
|
FileHelper.write_text_file_noblank(json, FileHelper.get_temp_file(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun save_temp_suspended(context: Context)
|
||||||
|
{
|
||||||
|
return suspendCoroutine { cont ->
|
||||||
|
cont.resume(this.save_temp(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun to_gpx(): String {
|
||||||
|
val gpxString = StringBuilder("")
|
||||||
|
|
||||||
|
// Header
|
||||||
|
gpxString.appendLine("""
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||||
|
<gpx
|
||||||
|
version="1.1" creator="Trackbook App (Android)"
|
||||||
|
xmlns="http://www.topografix.com/GPX/1/1"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
|
||||||
|
>
|
||||||
|
""".trimIndent())
|
||||||
|
gpxString.appendLine("\t<metadata>")
|
||||||
|
gpxString.appendLine("\t\t<name>Trackbook Recording: ${this.name}</name>")
|
||||||
|
gpxString.appendLine("\t</metadata>")
|
||||||
|
|
||||||
|
// POIs
|
||||||
|
val poiList: List<WayPoint> = this.wayPoints.filter { it.starred }
|
||||||
|
poiList.forEach { poi ->
|
||||||
|
gpxString.appendLine("\t<wpt lat=\"${poi.latitude}\" lon=\"${poi.longitude}\">")
|
||||||
|
gpxString.appendLine("\t\t<name>Point of interest</name>")
|
||||||
|
gpxString.appendLine("\t\t<ele>${poi.altitude}</ele>")
|
||||||
|
gpxString.appendLine("\t</wpt>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRK
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
|
||||||
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
gpxString.appendLine("\t<trk>")
|
||||||
|
gpxString.appendLine("\t\t<name>${this.name}</name>")
|
||||||
|
gpxString.appendLine("\t\t<trkseg>")
|
||||||
|
this.wayPoints.forEach { wayPoint ->
|
||||||
|
gpxString.appendLine("\t\t\t<trkpt lat=\"${wayPoint.latitude}\" lon=\"${wayPoint.longitude}\">")
|
||||||
|
gpxString.appendLine("\t\t\t\t<ele>${wayPoint.altitude}</ele>")
|
||||||
|
gpxString.appendLine("\t\t\t\t<time>${dateFormat.format(Date(wayPoint.time))}</time>")
|
||||||
|
gpxString.appendLine("\t\t\t\t<sat>${wayPoint.numberSatellites}</sat>")
|
||||||
|
gpxString.appendLine("\t\t\t</trkpt>")
|
||||||
|
}
|
||||||
|
gpxString.appendLine("\t\t</trkseg>")
|
||||||
|
gpxString.appendLine("\t</trk>")
|
||||||
|
gpxString.appendLine("</gpx>")
|
||||||
|
|
||||||
|
return gpxString.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun to_json(): String
|
||||||
|
{
|
||||||
|
return FileHelper.getCustomGson().toJson(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load_temp_track(context: Context): Track
|
||||||
|
{
|
||||||
|
return track_from_file(context, FileHelper.get_temp_file(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun track_from_file(context: Context, file: File): Track
|
||||||
|
{
|
||||||
|
// get JSON from text file
|
||||||
|
val json: String = FileHelper.readTextFile(context, file)
|
||||||
|
if (json.isEmpty())
|
||||||
|
{
|
||||||
|
return Track()
|
||||||
|
}
|
||||||
|
return FileHelper.getCustomGson().fromJson(json, Track::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun make_random_id(): Long
|
fun make_random_id(): Long
|
||||||
|
|
|
@ -17,54 +17,79 @@
|
||||||
|
|
||||||
package org.y20k.trackbook.core
|
package org.y20k.trackbook.core
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import com.google.gson.annotations.Expose
|
import com.google.gson.annotations.Expose
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.y20k.trackbook.Keys
|
import org.y20k.trackbook.Keys
|
||||||
import org.y20k.trackbook.helpers.TrackHelper
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Tracklist data class
|
* Tracklist data class
|
||||||
*/
|
*/
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Tracklist (@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION,
|
data class Tracklist (
|
||||||
@Expose val tracklistElements: MutableList<TracklistElement> = mutableListOf<TracklistElement>()): Parcelable {
|
@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION,
|
||||||
|
@Expose val tracks: MutableList<Track> = mutableListOf<Track>()
|
||||||
/* Return trackelement for given track id */
|
): Parcelable
|
||||||
fun getTrackElement(trackId: Long): TracklistElement? {
|
{
|
||||||
tracklistElements.forEach { tracklistElement ->
|
fun delete_non_starred(context: Context)
|
||||||
if (tracklistElement.id == trackId) {
|
{
|
||||||
return tracklistElement
|
val to_delete: List<Track> = this.tracks.filter{! it.starred}
|
||||||
|
to_delete.forEach { track ->
|
||||||
|
if (!track.starred)
|
||||||
|
{
|
||||||
|
track.delete(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
this.tracks.removeIf{! it.starred}
|
||||||
|
}
|
||||||
|
suspend fun delete_non_starred_suspended(context: Context)
|
||||||
|
{
|
||||||
|
return suspendCoroutine { cont ->
|
||||||
|
cont.resume(this.delete_non_starred(context))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get_total_distance(): Float
|
fun get_total_distance(): Double
|
||||||
{
|
{
|
||||||
var total: Float = 0F
|
return this.tracks.sumOf {it.distance.toDouble()}
|
||||||
tracklistElements.forEach { tracklist_element ->
|
|
||||||
total += tracklist_element.distance
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get_total_duration(): Long
|
fun get_total_duration(): Long
|
||||||
{
|
{
|
||||||
var total: Long = 0L
|
return this.tracks.sumOf {it.duration}
|
||||||
tracklistElements.forEach { tracklist_element ->
|
|
||||||
total += tracklist_element.duration
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create a deep copy */
|
fun deepCopy(): Tracklist
|
||||||
fun deepCopy(): Tracklist {
|
{
|
||||||
return Tracklist(tracklistFormatVersion, mutableListOf<TracklistElement>().apply { addAll(tracklistElements) })
|
return Tracklist(tracklistFormatVersion, mutableListOf<Track>().apply { addAll(tracks) })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun load_tracklist(context: Context): Tracklist {
|
||||||
|
Log.i("VOUSSOIR", "Loading tracklist.")
|
||||||
|
val folder = context.getExternalFilesDir("tracks")
|
||||||
|
var tracklist: Tracklist = Tracklist()
|
||||||
|
if (folder == null)
|
||||||
|
{
|
||||||
|
return tracklist
|
||||||
|
}
|
||||||
|
folder.walk().filter{ f: File -> f.isFile }.forEach{ json_file ->
|
||||||
|
val track = track_from_file(context, json_file)
|
||||||
|
tracklist.tracks.add(track)
|
||||||
|
}
|
||||||
|
tracklist.tracks.sortByDescending {it.recordingStart}
|
||||||
|
return tracklist
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun load_tracklist_suspended(context: Context): Tracklist
|
||||||
|
{
|
||||||
|
return suspendCoroutine {cont -> cont.resume(load_tracklist(context))}
|
||||||
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
* TracklistElement.kt
|
|
||||||
* Implements the TracklistElement data class
|
|
||||||
* A TracklistElement data about a Track
|
|
||||||
*
|
|
||||||
* This file is part of
|
|
||||||
* TRACKBOOK - Movement Recorder for Android
|
|
||||||
*
|
|
||||||
* Copyright (c) 2016-22 - Y20K.org
|
|
||||||
* Licensed under the MIT-License
|
|
||||||
* http://opensource.org/licenses/MIT
|
|
||||||
*
|
|
||||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
|
||||||
* https://github.com/osmdroid/osmdroid
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package org.y20k.trackbook.core
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.annotation.Keep
|
|
||||||
import com.google.gson.annotations.Expose
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TracklistElement data class
|
|
||||||
*/
|
|
||||||
@Keep
|
|
||||||
@Parcelize
|
|
||||||
data class TracklistElement(
|
|
||||||
@Expose val id: Long,
|
|
||||||
@Expose var name: String,
|
|
||||||
@Expose val date: Date,
|
|
||||||
@Expose val dateString: String,
|
|
||||||
@Expose val duration: Long,
|
|
||||||
@Expose val distance: Float,
|
|
||||||
@Expose val trackUriString: String,
|
|
||||||
@Expose val gpxUriString: String,
|
|
||||||
@Expose var starred: Boolean = false
|
|
||||||
) : Parcelable {
|
|
||||||
}
|
|
|
@ -18,26 +18,16 @@
|
||||||
package org.y20k.trackbook.helpers
|
package org.y20k.trackbook.helpers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.net.toFile
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import org.y20k.trackbook.Keys
|
|
||||||
import org.y20k.trackbook.core.Track
|
|
||||||
import org.y20k.trackbook.core.Tracklist
|
|
||||||
import org.y20k.trackbook.core.TracklistElement
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.text.NumberFormat
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlin.math.ln
|
import org.y20k.trackbook.Keys
|
||||||
import kotlin.math.pow
|
import org.y20k.trackbook.core.Track
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -48,185 +38,29 @@ object FileHelper {
|
||||||
/* Define log tag */
|
/* Define log tag */
|
||||||
private val TAG: String = LogHelper.makeLogTag(FileHelper::class.java)
|
private val TAG: String = LogHelper.makeLogTag(FileHelper::class.java)
|
||||||
|
|
||||||
|
fun delete_temp_file(context: Context)
|
||||||
/* Return an InputStream for given Uri */
|
{
|
||||||
fun getTextFileStream(context: Context, uri: Uri): InputStream? {
|
val temp: File = get_temp_file(context)
|
||||||
var stream : InputStream? = null
|
if (temp.isFile())
|
||||||
try {
|
|
||||||
stream = context.contentResolver.openInputStream(uri)
|
|
||||||
} catch (e : Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Get file size for given Uri */
|
|
||||||
fun getFileSize(context: Context, uri: Uri): Long {
|
|
||||||
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
|
|
||||||
if (cursor != null) {
|
|
||||||
val sizeIndex: Int = cursor.getColumnIndex(OpenableColumns.SIZE)
|
|
||||||
cursor.moveToFirst()
|
|
||||||
val size: Long = cursor.getLong(sizeIndex)
|
|
||||||
cursor.close()
|
|
||||||
return size
|
|
||||||
} else {
|
|
||||||
return 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Get file name for given Uri */
|
|
||||||
fun getFileName(context: Context, uri: Uri): String {
|
|
||||||
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
|
|
||||||
if (cursor != null) {
|
|
||||||
val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
||||||
cursor.moveToFirst()
|
|
||||||
val name: String = cursor.getString(nameIndex)
|
|
||||||
cursor.close()
|
|
||||||
return name
|
|
||||||
} else {
|
|
||||||
return String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Clears given folder - keeps given number of files */
|
|
||||||
fun clearFolder(folder: File?, keep: Int, deleteFolder: Boolean = false) {
|
|
||||||
if (folder != null && folder.exists()) {
|
|
||||||
val files = folder.listFiles()
|
|
||||||
val fileCount: Int = files.size
|
|
||||||
files.sortBy { it.lastModified() }
|
|
||||||
for (fileNumber in files.indices) {
|
|
||||||
if (fileNumber < fileCount - keep) {
|
|
||||||
files[fileNumber].delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (deleteFolder && keep == 0) {
|
|
||||||
folder.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Reads tracklist from storage using GSON */
|
|
||||||
fun readTracklist(context: Context): Tracklist {
|
|
||||||
LogHelper.v(TAG, "Reading Tracklist - Thread: ${Thread.currentThread().name}")
|
|
||||||
var folder = context.getExternalFilesDir("tracks")
|
|
||||||
var tracklist: Tracklist = Tracklist()
|
|
||||||
Log.i(TAG, folder.toString())
|
|
||||||
if (folder != null)
|
|
||||||
{
|
{
|
||||||
folder.walk().filter{ f: File -> f.isFile }.forEach{ track_file ->
|
temp.delete()
|
||||||
val track_json = readTextFile(context, track_file.toUri())
|
|
||||||
Log.i("VOUSSOIR", track_json)
|
|
||||||
val track = getCustomGson().fromJson(track_json, Track::class.java)
|
|
||||||
val tracklist_element = track.toTracklistElement(context)
|
|
||||||
tracklist.tracklistElements.add(tracklist_element)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return tracklist
|
|
||||||
}
|
}
|
||||||
|
fun get_temp_file(context: Context): File
|
||||||
|
{
|
||||||
/* Reads track from storage using GSON */
|
return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE)
|
||||||
fun readTrack(context: Context, fileUri: Uri): Track {
|
|
||||||
// get JSON from text file
|
|
||||||
val json: String = readTextFile(context, fileUri)
|
|
||||||
var track: Track = Track()
|
|
||||||
when (json.isNotEmpty()) {
|
|
||||||
// convert JSON and return as track
|
|
||||||
true -> try {
|
|
||||||
track = getCustomGson().fromJson(json, Track::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Deletes temp track file */
|
|
||||||
fun deleteTempFile(context: Context) {
|
|
||||||
getTempFileUri(context).toFile().delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Checks if temp track file exists */
|
|
||||||
fun tempFileExists(context: Context): Boolean {
|
|
||||||
return getTempFileUri(context).toFile().exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Creates Uri for Gpx file of a track */
|
|
||||||
fun getGpxFileUri(context: Context, track: Track): Uri = File(context.getExternalFilesDir(Keys.FOLDER_GPX), getGpxFileName(track)).toUri()
|
|
||||||
|
|
||||||
|
|
||||||
/* Creates file name for Gpx file of a track */
|
|
||||||
fun getGpxFileName(track: Track): String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.GPX_FILE_EXTENSION
|
|
||||||
|
|
||||||
|
|
||||||
/* Creates Uri for json track file */
|
|
||||||
fun getTrackFileUri(context: Context, track: Track): Uri {
|
|
||||||
val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.TRACKBOOK_FILE_EXTENSION
|
|
||||||
return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), fileName).toUri()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Creates Uri for json temp track file */
|
|
||||||
fun getTempFileUri(context: Context): Uri {
|
|
||||||
return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE).toUri()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Suspend function: Wrapper for renameTrack */
|
/* Suspend function: Wrapper for renameTrack */
|
||||||
suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) {
|
suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) {
|
||||||
return suspendCoroutine { cont ->
|
return suspendCoroutine { cont ->
|
||||||
cont.resume(renameTrack(context, track, newName))
|
track.name = newName
|
||||||
|
track.save_both(context)
|
||||||
|
cont.resume(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Suspend function: Wrapper for saveTrack */
|
|
||||||
suspend fun saveTrackSuspended(track: Track, saveGpxToo: Boolean) {
|
|
||||||
return suspendCoroutine { cont ->
|
|
||||||
cont.resume(saveTrack(track, saveGpxToo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Suspend function: Wrapper for saveTempTrack */
|
|
||||||
suspend fun saveTempTrackSuspended(context: Context, track: Track) {
|
|
||||||
return suspendCoroutine { cont ->
|
|
||||||
cont.resume(saveTempTrack(context, track))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Suspend function: Wrapper for deleteTrack */
|
|
||||||
suspend fun deleteTrackSuspended(context: Context, tracklist_element: TracklistElement, tracklist: Tracklist): Tracklist {
|
|
||||||
return suspendCoroutine { cont ->
|
|
||||||
cont.resume(deleteTrack(context, tracklist_element, tracklist))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Suspend function: Deletes tracks that are not starred using deleteTracks */
|
|
||||||
suspend fun deleteNonStarredSuspended(context: Context, tracklist: Tracklist): Tracklist {
|
|
||||||
return suspendCoroutine { cont ->
|
|
||||||
val tracklistElements = mutableListOf<TracklistElement>()
|
|
||||||
tracklist.tracklistElements.forEach { tracklistElement ->
|
|
||||||
if (!tracklistElement.starred) {
|
|
||||||
tracklistElements.add(tracklistElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cont.resume(deleteTracks(context, tracklistElements, tracklist))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Suspend function: Wrapper for readTracklist */
|
|
||||||
suspend fun readTracklistSuspended(context: Context): Tracklist {
|
|
||||||
return suspendCoroutine {cont ->
|
|
||||||
cont.resume(readTracklist(context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Suspend function: Wrapper for copyFile */
|
/* Suspend function: Wrapper for copyFile */
|
||||||
suspend fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
|
suspend fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
|
||||||
|
@ -235,83 +69,6 @@ object FileHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Save Track as JSON to storage */
|
|
||||||
private fun saveTrack(track: Track, saveGpxToo: Boolean) {
|
|
||||||
val jsonString: String = getTrackJsonString(track)
|
|
||||||
if (jsonString.isNotBlank()) {
|
|
||||||
// write track file
|
|
||||||
writeTextFile(jsonString, track.trackUriString.toUri())
|
|
||||||
}
|
|
||||||
if (saveGpxToo) {
|
|
||||||
val gpxString: String = TrackHelper.createGpxString(track)
|
|
||||||
if (gpxString.isNotBlank()) {
|
|
||||||
// write GPX file
|
|
||||||
writeTextFile(gpxString, track.gpxUriString.toUri())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Save Temp Track as JSON to storage */
|
|
||||||
private fun saveTempTrack(context: Context, track: Track) {
|
|
||||||
val json: String = getTrackJsonString(track)
|
|
||||||
if (json.isNotBlank()) {
|
|
||||||
writeTextFile(json, getTempFileUri(context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Creates Uri for tracklist file */
|
|
||||||
private fun getTracklistFileUri(context: Context): Uri {
|
|
||||||
return File(context.getExternalFilesDir(""), Keys.TRACKLIST_FILE).toUri()
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Renames track */
|
|
||||||
private fun renameTrack(context: Context, track: Track, newName: String) {
|
|
||||||
// search track in tracklist
|
|
||||||
val tracklist: Tracklist = readTracklist(context)
|
|
||||||
var trackUriString: String = String()
|
|
||||||
tracklist.tracklistElements.forEach { tracklistElement ->
|
|
||||||
if (tracklistElement.id == track.id) {
|
|
||||||
// rename tracklist element
|
|
||||||
tracklistElement.name = newName
|
|
||||||
trackUriString = tracklistElement.trackUriString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (trackUriString.isNotEmpty()) {
|
|
||||||
// rename track
|
|
||||||
track.name = newName
|
|
||||||
// save track
|
|
||||||
saveTrack(track, saveGpxToo = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Deletes multiple tracks */
|
|
||||||
private fun deleteTracks(context: Context, tracklistElements: MutableList<TracklistElement>, tracklist: Tracklist): Tracklist {
|
|
||||||
tracklistElements.forEach { tracklistElement ->
|
|
||||||
deleteTrack(context, tracklistElement, tracklist)
|
|
||||||
}
|
|
||||||
return tracklist
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Deletes one track */
|
|
||||||
private fun deleteTrack(context: Context, tracklist_element: TracklistElement, tracklist: Tracklist): Tracklist {
|
|
||||||
// delete track files
|
|
||||||
val json_file: File = tracklist_element.trackUriString.toUri().toFile()
|
|
||||||
if (json_file.isFile)
|
|
||||||
{
|
|
||||||
json_file.delete()
|
|
||||||
}
|
|
||||||
val gpx_file: File = tracklist_element.gpxUriString.toUri().toFile()
|
|
||||||
if (gpx_file.isFile)
|
|
||||||
{
|
|
||||||
gpx_file.delete()
|
|
||||||
}
|
|
||||||
// remove track element from list
|
|
||||||
tracklist.tracklistElements.removeIf {it.id == tracklist_element.id}
|
|
||||||
return tracklist
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Copies file to specified target */
|
/* Copies file to specified target */
|
||||||
private fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
|
private fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
|
||||||
|
@ -325,58 +82,19 @@ object FileHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCustomGson(): Gson
|
||||||
/* Converts track to JSON */
|
{
|
||||||
private fun getTrackJsonString(track: Track): String {
|
|
||||||
val gson: Gson = getCustomGson()
|
|
||||||
var json: String = String()
|
|
||||||
try {
|
|
||||||
json = gson.toJson(track)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Creates a Gson object */
|
|
||||||
private fun getCustomGson(): Gson {
|
|
||||||
val gsonBuilder = GsonBuilder()
|
val gsonBuilder = GsonBuilder()
|
||||||
gsonBuilder.setDateFormat("yyyy-MM-dd-HH-mm-ss")
|
gsonBuilder.setDateFormat("yyyy-MM-dd-HH-mm-ss")
|
||||||
gsonBuilder.excludeFieldsWithoutExposeAnnotation()
|
gsonBuilder.excludeFieldsWithoutExposeAnnotation()
|
||||||
return gsonBuilder.create()
|
return gsonBuilder.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Converts byte value into a human readable format */
|
|
||||||
// Source: https://programming.guide/java/formatting-byte-size-to-human-readable-format.html
|
|
||||||
fun getReadableByteCount(bytes: Long, si: Boolean = true): String {
|
|
||||||
|
|
||||||
// check if Decimal prefix symbol (SI) or Binary prefix symbol (IEC) requested
|
|
||||||
val unit: Long = if (si) 1000L else 1024L
|
|
||||||
|
|
||||||
// just return bytes if file size is smaller than requested unit
|
|
||||||
if (bytes < unit) return "$bytes B"
|
|
||||||
|
|
||||||
// calculate exp
|
|
||||||
val exp: Int = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
|
||||||
|
|
||||||
// determine prefix symbol
|
|
||||||
val prefix: String = ((if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i")
|
|
||||||
|
|
||||||
// calculate result and set number format
|
|
||||||
val result: Double = bytes / unit.toDouble().pow(exp.toDouble())
|
|
||||||
val numberFormat = NumberFormat.getNumberInstance()
|
|
||||||
numberFormat.maximumFractionDigits = 1
|
|
||||||
|
|
||||||
return numberFormat.format(result) + " " + prefix + "B"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Reads InputStream from file uri and returns it as String */
|
/* Reads InputStream from file uri and returns it as String */
|
||||||
private fun readTextFile(context: Context, fileUri: Uri): String {
|
fun readTextFile(context: Context, file: File): String {
|
||||||
// todo read https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html
|
// todo read https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html
|
||||||
// https://developer.android.com/training/secure-file-sharing/retrieve-info
|
// https://developer.android.com/training/secure-file-sharing/retrieve-info
|
||||||
val file: File = fileUri.toFile()
|
|
||||||
// check if file exists
|
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
return String()
|
return String()
|
||||||
}
|
}
|
||||||
|
@ -393,27 +111,12 @@ object FileHelper {
|
||||||
|
|
||||||
|
|
||||||
/* Writes given text to file on storage */
|
/* Writes given text to file on storage */
|
||||||
private fun writeTextFile(text: String, fileUri: Uri) {
|
fun write_text_file_noblank(text: String, file: File)
|
||||||
|
{
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
val file: File = fileUri.toFile()
|
|
||||||
file.writeText(text)
|
file.writeText(text)
|
||||||
} else {
|
} else {
|
||||||
LogHelper.w(TAG, "Writing text file $fileUri failed. Empty text string text was provided.")
|
LogHelper.w(TAG, "Writing text file ${file.toUri()} failed. Empty text string was provided.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Writes given bitmap as image file to storage */
|
|
||||||
private fun writeImageFile(context: Context, bitmap: Bitmap, file: File, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, quality: Int = 75) {
|
|
||||||
if (file.exists()) file.delete ()
|
|
||||||
try {
|
|
||||||
val out = FileOutputStream(file)
|
|
||||||
bitmap.compress(format, quality, out)
|
|
||||||
out.flush()
|
|
||||||
out.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,20 +17,10 @@
|
||||||
package org.y20k.trackbook.helpers
|
package org.y20k.trackbook.helpers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.Location
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.net.toUri
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.y20k.trackbook.Keys
|
|
||||||
import org.y20k.trackbook.R
|
import org.y20k.trackbook.R
|
||||||
import org.y20k.trackbook.core.Track
|
import org.y20k.trackbook.core.Track
|
||||||
import org.y20k.trackbook.core.Tracklist
|
|
||||||
import org.y20k.trackbook.core.TracklistElement
|
|
||||||
import org.y20k.trackbook.core.WayPoint
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TrackHelper object
|
* TrackHelper object
|
||||||
|
@ -41,123 +31,14 @@ object TrackHelper {
|
||||||
private val TAG: String = LogHelper.makeLogTag(TrackHelper::class.java)
|
private val TAG: String = LogHelper.makeLogTag(TrackHelper::class.java)
|
||||||
|
|
||||||
/* Adds given locatiom as waypoint to track */
|
/* Adds given locatiom as waypoint to track */
|
||||||
fun addWayPointToTrack(track: Track, location: Location, omitRests: Boolean, resumed: Boolean): Pair<Boolean, Track> {
|
|
||||||
// Step 1: Get previous location
|
|
||||||
val previousLocation: Location?
|
|
||||||
var numberOfWayPoints: Int = track.wayPoints.size
|
|
||||||
|
|
||||||
// CASE: First location
|
|
||||||
if (numberOfWayPoints == 0) {
|
|
||||||
previousLocation = null
|
|
||||||
}
|
|
||||||
// CASE: Second location - check if first location was plausible & remove implausible location
|
|
||||||
else if (numberOfWayPoints == 1 && !LocationHelper.isFirstLocationPlausible(location, track)) {
|
|
||||||
previousLocation = null
|
|
||||||
numberOfWayPoints = 0
|
|
||||||
track.wayPoints.removeAt(0)
|
|
||||||
}
|
|
||||||
// CASE: Third location or second location (if first was plausible)
|
|
||||||
else {
|
|
||||||
previousLocation = track.wayPoints[numberOfWayPoints - 1].toLocation()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Update duration
|
|
||||||
val now: Date = GregorianCalendar.getInstance().time
|
|
||||||
val difference: Long = now.time - track.recordingStop.time
|
|
||||||
track.duration = track.duration + difference
|
|
||||||
track.recordingStop = now
|
|
||||||
|
|
||||||
// Step 3: Add waypoint, ifrecent and accurate and different enough
|
|
||||||
val shouldBeAdded: Boolean = (
|
|
||||||
LocationHelper.isRecentEnough(location) &&
|
|
||||||
LocationHelper.isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY) &&
|
|
||||||
LocationHelper.isDifferentEnough(previousLocation, location, omitRests)
|
|
||||||
)
|
|
||||||
if (shouldBeAdded) {
|
|
||||||
// Step 3.1: Update distance (do not update if resumed -> we do not want to add values calculated during a recording pause)
|
|
||||||
if (!resumed) {
|
|
||||||
track.distance = track.distance + LocationHelper.calculateDistance(previousLocation, location)
|
|
||||||
}
|
|
||||||
// Step 3.2: Update altitude values
|
|
||||||
val altitude: Double = location.altitude
|
|
||||||
if (altitude != 0.0) {
|
|
||||||
if (numberOfWayPoints == 0) {
|
|
||||||
track.maxAltitude = altitude
|
|
||||||
track.minAltitude = altitude
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (altitude > track.maxAltitude) track.maxAltitude = altitude
|
|
||||||
if (altitude < track.minAltitude) track.minAltitude = altitude
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Step 3.3: Toggle stop over status, if necessary
|
|
||||||
if (track.wayPoints.size < 0) {
|
|
||||||
track.wayPoints[track.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3.4: Add current location as point to center on for later display
|
|
||||||
track.latitude = location.latitude
|
|
||||||
track.longitude = location.longitude
|
|
||||||
|
|
||||||
// Step 3.5: Add location as new waypoint
|
|
||||||
track.wayPoints.add(WayPoint(location = location, distanceToStartingPoint = track.distance))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pair(shouldBeAdded, track)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Calculates time passed since last stop of recording */
|
/* Calculates time passed since last stop of recording */
|
||||||
fun calculateDurationOfPause(recordingStop: Date): Long = GregorianCalendar.getInstance().time.time - recordingStop.time
|
fun calculateDurationOfPause(recordingStop: Date): Long = GregorianCalendar.getInstance().time.time - recordingStop.time
|
||||||
|
|
||||||
/* Creates GPX string for given track */
|
|
||||||
fun createGpxString(track: Track): String {
|
|
||||||
val gpxString = StringBuilder("")
|
|
||||||
|
|
||||||
// Header
|
|
||||||
gpxString.appendLine("""
|
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
|
||||||
<gpx
|
|
||||||
version="1.1" creator="Trackbook App (Android)"
|
|
||||||
xmlns="http://www.topografix.com/GPX/1/1"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
|
|
||||||
>
|
|
||||||
""".trimIndent())
|
|
||||||
gpxString.appendLine("\t<metadata>")
|
|
||||||
gpxString.appendLine("\t\t<name>Trackbook Recording: ${track.name}</name>")
|
|
||||||
gpxString.appendLine("\t</metadata>")
|
|
||||||
|
|
||||||
// POIs
|
|
||||||
val poiList: List<WayPoint> = track.wayPoints.filter { it.starred }
|
|
||||||
poiList.forEach { poi ->
|
|
||||||
gpxString.appendLine("\t<wpt lat=\"${poi.latitude}\" lon=\"${poi.longitude}\">")
|
|
||||||
gpxString.appendLine("\t\t<name>Point of interest</name>")
|
|
||||||
gpxString.appendLine("\t\t<ele>${poi.altitude}</ele>")
|
|
||||||
gpxString.appendLine("\t</wpt>")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TRK
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
|
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
gpxString.appendLine("\t<trk>")
|
|
||||||
gpxString.appendLine("\t\t<name>${track.name}</name>")
|
|
||||||
gpxString.appendLine("\t\t<trkseg>")
|
|
||||||
track.wayPoints.forEach { wayPoint ->
|
|
||||||
gpxString.appendLine("\t\t\t<trkpt lat=\"${wayPoint.latitude}\" lon=\"${wayPoint.longitude}\">")
|
|
||||||
gpxString.appendLine("\t\t\t\t<ele>${wayPoint.altitude}</ele>")
|
|
||||||
gpxString.appendLine("\t\t\t\t<time>${dateFormat.format(Date(wayPoint.time))}</time>")
|
|
||||||
gpxString.appendLine("\t\t\t\t<sat>${wayPoint.numberSatellites}</sat>")
|
|
||||||
gpxString.appendLine("\t\t\t</trkpt>")
|
|
||||||
}
|
|
||||||
gpxString.appendLine("\t\t</trkseg>")
|
|
||||||
gpxString.appendLine("\t</trk>")
|
|
||||||
gpxString.appendLine("</gpx>")
|
|
||||||
|
|
||||||
return gpxString.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggles starred flag for given position */
|
/* Toggles starred flag for given position */
|
||||||
fun toggleStarred(context: Context, track: Track, latitude: Double, longitude: Double): Track {
|
fun toggle_waypoint_starred(context: Context, track: Track, latitude: Double, longitude: Double)
|
||||||
|
{
|
||||||
track.wayPoints.forEach { waypoint ->
|
track.wayPoints.forEach { waypoint ->
|
||||||
if (waypoint.latitude == latitude && waypoint.longitude == longitude) {
|
if (waypoint.latitude == latitude && waypoint.longitude == longitude) {
|
||||||
waypoint.starred = !waypoint.starred
|
waypoint.starred = !waypoint.starred
|
||||||
|
@ -167,6 +48,5 @@ object TrackHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return track
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import java.util.*
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
@ -38,8 +37,9 @@ import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import org.y20k.trackbook.Keys
|
import org.y20k.trackbook.Keys
|
||||||
import org.y20k.trackbook.R
|
import org.y20k.trackbook.R
|
||||||
|
import org.y20k.trackbook.core.Track
|
||||||
import org.y20k.trackbook.core.Tracklist
|
import org.y20k.trackbook.core.Tracklist
|
||||||
import org.y20k.trackbook.core.TracklistElement
|
import org.y20k.trackbook.core.load_tracklist
|
||||||
import org.y20k.trackbook.helpers.*
|
import org.y20k.trackbook.helpers.*
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,24 +61,20 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
||||||
|
|
||||||
/* Listener Interface */
|
/* Listener Interface */
|
||||||
interface TracklistAdapterListener {
|
interface TracklistAdapterListener {
|
||||||
fun onTrackElementTapped(tracklistElement: TracklistElement) { }
|
fun onTrackElementTapped(track: Track) { }
|
||||||
// fun onTrackElementStarred(trackId: Long, starred: Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onAttachedToRecyclerView from RecyclerView.Adapter */
|
override fun onAttachedToRecyclerView(recyclerView: RecyclerView)
|
||||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
{
|
||||||
// get reference to listener
|
|
||||||
tracklistListener = fragment as TracklistAdapterListener
|
tracklistListener = fragment as TracklistAdapterListener
|
||||||
// load tracklist
|
tracklist = load_tracklist(context)
|
||||||
tracklist = FileHelper.readTracklist(context)
|
|
||||||
tracklist.tracklistElements.sortByDescending { tracklistElement -> tracklistElement.date }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onCreateViewHolder from RecyclerView.Adapter */
|
/* Overrides onCreateViewHolder from RecyclerView.Adapter */
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
|
||||||
|
{
|
||||||
when (viewType) {
|
when (viewType) {
|
||||||
Keys.VIEW_TYPE_STATISTICS -> {
|
Keys.VIEW_TYPE_STATISTICS -> {
|
||||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.element_statistics, parent, false)
|
val v = LayoutInflater.from(parent.context).inflate(R.layout.element_statistics, parent, false)
|
||||||
|
@ -104,16 +100,16 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
||||||
|
|
||||||
/* Overrides getItemCount from RecyclerView.Adapter */
|
/* Overrides getItemCount from RecyclerView.Adapter */
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
// +1 ==> the total statistics element
|
// +1 because of the total statistics element
|
||||||
return tracklist.tracklistElements.size + 1
|
return tracklist.tracks.size + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onBindViewHolder from RecyclerView.Adapter */
|
/* Overrides onBindViewHolder from RecyclerView.Adapter */
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
|
||||||
|
{
|
||||||
when (holder) {
|
when (holder)
|
||||||
|
{
|
||||||
// CASE STATISTICS ELEMENT
|
// CASE STATISTICS ELEMENT
|
||||||
is ElementStatisticsViewHolder -> {
|
is ElementStatisticsViewHolder -> {
|
||||||
val elementStatisticsViewHolder: ElementStatisticsViewHolder = holder as ElementStatisticsViewHolder
|
val elementStatisticsViewHolder: ElementStatisticsViewHolder = holder as ElementStatisticsViewHolder
|
||||||
|
@ -124,14 +120,14 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
||||||
is ElementTrackViewHolder -> {
|
is ElementTrackViewHolder -> {
|
||||||
val positionInTracklist: Int = position - 1 // Element 0 is the statistics element.
|
val positionInTracklist: Int = position - 1 // Element 0 is the statistics element.
|
||||||
val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder
|
val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder
|
||||||
elementTrackViewHolder.trackNameView.text = tracklist.tracklistElements[positionInTracklist].name
|
elementTrackViewHolder.trackNameView.text = tracklist.tracks[positionInTracklist].name
|
||||||
elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist)
|
elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist)
|
||||||
when (tracklist.tracklistElements[positionInTracklist].starred) {
|
when (tracklist.tracks[positionInTracklist].starred) {
|
||||||
true -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp))
|
true -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp))
|
||||||
false -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp))
|
false -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp))
|
||||||
}
|
}
|
||||||
elementTrackViewHolder.trackElement.setOnClickListener {
|
elementTrackViewHolder.trackElement.setOnClickListener {
|
||||||
tracklistListener.onTrackElementTapped(tracklist.tracklistElements[positionInTracklist])
|
tracklistListener.onTrackElementTapped(tracklist.tracks[positionInTracklist])
|
||||||
}
|
}
|
||||||
elementTrackViewHolder.starButton.setOnClickListener {
|
elementTrackViewHolder.starButton.setOnClickListener {
|
||||||
toggleStarred(it, positionInTracklist)
|
toggleStarred(it, positionInTracklist)
|
||||||
|
@ -144,102 +140,76 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
||||||
|
|
||||||
|
|
||||||
/* Get track name for given position */
|
/* Get track name for given position */
|
||||||
fun getTrackName(positionInRecyclerView: Int): String {
|
fun getTrackName(positionInRecyclerView: Int): String
|
||||||
// first position is always the statistics element
|
{
|
||||||
return tracklist.tracklistElements[positionInRecyclerView - 1].name
|
// Minus 1 because first position is always the statistics element
|
||||||
|
return tracklist.tracks[positionInRecyclerView - 1].name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun delete_track_at_position(context: Context, ui_index: Int)
|
||||||
/* Removes track and track files for given position - used by TracklistFragment */
|
{
|
||||||
fun removeTrackAtPosition(context: Context, position: Int) {
|
|
||||||
CoroutineScope(IO).launch {
|
CoroutineScope(IO).launch {
|
||||||
val index = position - 1 // position 0 is the statistics element
|
val track_index = ui_index - 1 // position 0 is the statistics element
|
||||||
val tracklist_element = tracklist.tracklistElements[index]
|
val track = tracklist.tracks[track_index]
|
||||||
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, tracklist_element, tracklist) }
|
val deferred: Deferred<Unit> = async { track.delete_suspended(context) }
|
||||||
// wait for result and store in tracklist
|
// wait for result and store in tracklist
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
tracklist = deferred.await()
|
deferred.await()
|
||||||
|
tracklist.tracks.remove(track)
|
||||||
notifyItemChanged(0)
|
notifyItemChanged(0)
|
||||||
notifyItemRemoved(position)
|
notifyItemRemoved(ui_index)
|
||||||
notifyItemRangeChanged(position, tracklist.tracklistElements.size)
|
notifyItemRangeChanged(ui_index, tracklist.tracks.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Suspend function: Wrapper for removeTrackAtPosition */
|
suspend fun delete_track_at_position_suspended(context: Context, position: Int) {
|
||||||
suspend fun removeTrackAtPositionSuspended(context: Context, position: Int) {
|
|
||||||
return suspendCoroutine { cont ->
|
return suspendCoroutine { cont ->
|
||||||
cont.resume(removeTrackAtPosition(context, position))
|
cont.resume(delete_track_at_position(context, position))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Removes track and track files for given track id - used by TracklistFragment */
|
fun delete_track_by_id(context: Context, trackId: Long) {
|
||||||
fun removeTrackById(context: Context, trackId: Long) {
|
|
||||||
CoroutineScope(IO).launch {
|
CoroutineScope(IO).launch {
|
||||||
// reload tracklist //todo check if necessary
|
val index: Int = tracklist.tracks.indexOfFirst {it.id == trackId}
|
||||||
tracklist = FileHelper.readTracklist(context)
|
|
||||||
val index: Int = tracklist.tracklistElements.indexOfFirst {it.id == trackId}
|
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val tracklist_element = tracklist.tracklistElements[index]
|
delete_track_at_position(context, index + 1)
|
||||||
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, tracklist_element, tracklist) }
|
|
||||||
// wait for result and store in tracklist
|
|
||||||
val position = index + 1 // position 0 is the statistics element
|
|
||||||
withContext(Main) {
|
|
||||||
tracklist = deferred.await()
|
|
||||||
notifyItemChanged(0)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
notifyItemRangeChanged(position, tracklist.tracklistElements.size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Returns if the adapter is empty */
|
/* Returns if the adapter is empty */
|
||||||
fun isEmpty(): Boolean {
|
fun isEmpty(): Boolean {
|
||||||
return tracklist.tracklistElements.size == 0
|
return tracklist.tracks.size == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Finds current position of track element in adapter list */
|
|
||||||
private fun findPosition(trackId: Long): Int {
|
|
||||||
tracklist.tracklistElements.forEachIndexed {index, tracklistElement ->
|
|
||||||
if (tracklistElement.id == trackId)
|
|
||||||
{
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Toggles the starred state of tracklist element - and saves tracklist */
|
/* Toggles the starred state of tracklist element - and saves tracklist */
|
||||||
private fun toggleStarred(view: View, position: Int) {
|
private fun toggleStarred(view: View, position: Int) {
|
||||||
val starButton: ImageButton = view as ImageButton
|
val starButton: ImageButton = view as ImageButton
|
||||||
when (tracklist.tracklistElements[position].starred) {
|
if (tracklist.tracks[position].starred)
|
||||||
true -> {
|
{
|
||||||
starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp))
|
starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp))
|
||||||
tracklist.tracklistElements[position].starred = false
|
tracklist.tracks[position].starred = false
|
||||||
}
|
|
||||||
false -> {
|
|
||||||
starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp))
|
|
||||||
tracklist.tracklistElements[position].starred = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp))
|
||||||
|
tracklist.tracks[position].starred = true
|
||||||
|
}
|
||||||
|
tracklist.tracks[position].save_json(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Creates the track data string */
|
/* Creates the track data string */
|
||||||
private fun createTrackDataString(position: Int): String {
|
private fun createTrackDataString(position: Int): String {
|
||||||
val tracklistElement: TracklistElement = tracklist.tracklistElements[position]
|
val track: Track = tracklist.tracks[position]
|
||||||
val track_duration_string = DateTimeHelper.convertToReadableTime(context, tracklistElement.duration)
|
val track_duration_string = DateTimeHelper.convertToReadableTime(context, track.duration)
|
||||||
val trackDataString: String
|
val trackDataString: String
|
||||||
when (tracklistElement.name == tracklistElement.dateString) {
|
when (track.name == track.dateString) {
|
||||||
// CASE: no individual name set - exclude date
|
// CASE: no individual name set - exclude date
|
||||||
true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(tracklistElement.distance, useImperial)} • ${track_duration_string}"
|
true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
|
||||||
// CASE: no individual name set - include date
|
// CASE: no individual name set - include date
|
||||||
false -> trackDataString = "${tracklistElement.dateString} • ${LengthUnitHelper.convertDistanceToString(tracklistElement.distance, useImperial)} • ${track_duration_string}"
|
false -> trackDataString = "${track.dateString} • ${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
|
||||||
}
|
}
|
||||||
return trackDataString
|
return trackDataString
|
||||||
}
|
}
|
||||||
|
@ -251,22 +221,22 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
||||||
private inner class DiffCallback(val oldList: Tracklist, val newList: Tracklist): DiffUtil.Callback() {
|
private inner class DiffCallback(val oldList: Tracklist, val newList: Tracklist): DiffUtil.Callback() {
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldList.tracklistElements[oldItemPosition]
|
val oldItem = oldList.tracks[oldItemPosition]
|
||||||
val newItem = newList.tracklistElements[newItemPosition]
|
val newItem = newList.tracks[newItemPosition]
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOldListSize(): Int {
|
override fun getOldListSize(): Int {
|
||||||
return oldList.tracklistElements.size
|
return oldList.tracks.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNewListSize(): Int {
|
override fun getNewListSize(): Int {
|
||||||
return newList.tracklistElements.size
|
return newList.tracks.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldList.tracklistElements[oldItemPosition]
|
val oldItem = oldList.tracks[oldItemPosition]
|
||||||
val newItem = newList.tracklistElements[newItemPosition]
|
val newItem = newList.tracks[newItemPosition]
|
||||||
return (oldItem.id == newItem.id) && (oldItem.distance == newItem.distance)
|
return (oldItem.id == newItem.id) && (oldItem.distance == newItem.distance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,7 +187,7 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
|
||||||
mapView.overlays.add(trackSpecialMarkersOverlay)
|
mapView.overlays.add(trackSpecialMarkersOverlay)
|
||||||
}
|
}
|
||||||
// save track
|
// save track
|
||||||
CoroutineScope(Dispatchers.IO).launch { FileHelper.saveTrackSuspended(track, true) }
|
CoroutineScope(Dispatchers.IO).launch { track.save_both_suspended(context) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
|
||||||
{
|
{
|
||||||
if (track.latitude != 0.0 && track.longitude != 0.0)
|
if (track.latitude != 0.0 && track.longitude != 0.0)
|
||||||
{
|
{
|
||||||
CoroutineScope(Dispatchers.IO).launch { FileHelper.saveTrackSuspended(track, false) }
|
CoroutineScope(Dispatchers.IO).launch { track.save_json_suspended(context) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue