master
voussoir 2022-04-06 21:48:25 -07:00
parent 77cfcf202f
commit 0c510d4a11
No known key found for this signature in database
GPG Key ID: 5F7554F8C26DACCB
15 changed files with 214 additions and 311 deletions

View File

@ -275,10 +275,16 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
/* Saves track - shows dialog, if recording is still empty */ /* Saves track - shows dialog, if recording is still empty */
private fun saveTrack() { private fun saveTrack()
{
if (track.wayPoints.isEmpty()) if (track.wayPoints.isEmpty())
{ {
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_RESUME_EMPTY_RECORDING, message = R.string.dialog_error_empty_recording_message, yesButton = R.string.dialog_error_empty_recording_button_resume) YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(
context = activity as Context,
type = Keys.DIALOG_RESUME_EMPTY_RECORDING,
message = R.string.dialog_error_empty_recording_message,
yesButton = R.string.dialog_error_empty_recording_button_resume
)
} }
else else
{ {
@ -288,8 +294,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
track.gpxUriString = FileHelper.getGpxFileUri(activity as Context, track).toString() track.gpxUriString = FileHelper.getGpxFileUri(activity as Context, track).toString()
// step 2: save track // step 2: save track
FileHelper.saveTrackSuspended(track, saveGpxToo = true) FileHelper.saveTrackSuspended(track, saveGpxToo = true)
// step 3: save tracklist - suspended
FileHelper.addTrackAndSaveTracklistSuspended(activity as Context, track)
// step 3: clear track // step 3: clear track
trackerService.clearTrack() trackerService.clearTrack()
// step 4: open track in TrackFragement // step 4: open track in TrackFragement
@ -307,7 +311,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
bundle.putString(Keys.ARG_TRACK_TITLE, tracklistElement.name) bundle.putString(Keys.ARG_TRACK_TITLE, tracklistElement.name)
bundle.putString(Keys.ARG_TRACK_FILE_URI, tracklistElement.trackUriString) bundle.putString(Keys.ARG_TRACK_FILE_URI, tracklistElement.trackUriString)
bundle.putString(Keys.ARG_GPX_FILE_URI, tracklistElement.gpxUriString) bundle.putString(Keys.ARG_GPX_FILE_URI, tracklistElement.gpxUriString)
bundle.putLong(Keys.ARG_TRACK_ID, TrackHelper.getTrackId(tracklistElement)) bundle.putLong(Keys.ARG_TRACK_ID, tracklistElement.id)
findNavController().navigate(R.id.fragment_track, bundle) findNavController().navigate(R.id.fragment_track, bundle)
} }
@ -372,7 +376,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
// update location and track // update location and track
layout.markCurrentPosition(currentBestLocation, trackingState) layout.markCurrentPosition(currentBestLocation, trackingState)
layout.overlayCurrentTrack(track, trackingState) layout.overlayCurrentTrack(track, trackingState)
layout.updateLiveStatics(length = track.length, duration = track.duration, trackingState = trackingState) layout.updateLiveStatics(distance = track.distance, duration = track.duration, trackingState = trackingState)
// center map, if it had not been dragged/zoomed before // center map, if it had not been dragged/zoomed before
if (!layout.userInteraction) { layout.centerMap(currentBestLocation, true)} if (!layout.userInteraction) { layout.centerMap(currentBestLocation, true)}
// show error snackbar if necessary // show error snackbar if necessary

View File

@ -90,7 +90,12 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
// set up delete button // set up delete button
layout.deleteButton.setOnClickListener { layout.deleteButton.setOnClickListener {
val dialogMessage: String = "${getString(R.string.dialog_yes_no_message_delete_recording)}\n\n- ${layout.trackNameView.text}" val dialogMessage: String = "${getString(R.string.dialog_yes_no_message_delete_recording)}\n\n- ${layout.trackNameView.text}"
YesNoDialog(this@TrackFragment as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_DELETE_TRACK, messageString = dialogMessage, yesButton = R.string.dialog_yes_no_positive_button_delete_recording) YesNoDialog(this@TrackFragment as YesNoDialog.YesNoDialogListener).show(
context = activity as Context,
type = Keys.DIALOG_DELETE_TRACK,
messageString = dialogMessage,
yesButton = R.string.dialog_yes_no_positive_button_delete_recording
)
} }
// set up rename button // set up rename button
layout.editButton.setOnClickListener { layout.editButton.setOnClickListener {
@ -154,7 +159,7 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
// user tapped remove track // user tapped remove track
true -> { true -> {
// switch to TracklistFragment and remove track there // switch to TracklistFragment and remove track there
val bundle: Bundle = bundleOf(Keys.ARG_TRACK_ID to layout.track.getTrackId()) val bundle: Bundle = bundleOf(Keys.ARG_TRACK_ID to layout.track.id)
findNavController().navigate(R.id.tracklist_fragment, bundle) findNavController().navigate(R.id.tracklist_fragment, bundle)
} }
} }

View File

@ -14,17 +14,15 @@
* https://github.com/osmdroid/osmdroid * https://github.com/osmdroid/osmdroid
*/ */
package org.y20k.trackbook package org.y20k.trackbook
import android.Manifest
import android.app.Notification import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.SharedPreferences
import android.hardware.Sensor import android.hardware.Sensor
import android.hardware.SensorEvent import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
@ -32,16 +30,18 @@ import android.hardware.SensorManager
import android.location.Location import android.location.Location
import android.location.LocationListener import android.location.LocationListener
import android.location.LocationManager import android.location.LocationManager
import android.Manifest
import android.os.* import android.os.*
import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import java.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.Runnable
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
import org.y20k.trackbook.helpers.* import org.y20k.trackbook.helpers.*
import java.util.*
/* /*
* TrackerService class * TrackerService class
@ -51,7 +51,6 @@ class TrackerService: Service(), SensorEventListener {
/* Define log tag */ /* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackerService::class.java) private val TAG: String = LogHelper.makeLogTag(TrackerService::class.java)
/* Main class variables */ /* Main class variables */
var trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED var trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED
var gpsProviderActive: Boolean = false var gpsProviderActive: Boolean = false
@ -77,7 +76,6 @@ class TrackerService: Service(), SensorEventListener {
private lateinit var gpsLocationListener: LocationListener private lateinit var gpsLocationListener: LocationListener
private lateinit var networkLocationListener: LocationListener private lateinit var networkLocationListener: LocationListener
/* Overrides onCreate from Service */ /* Overrides onCreate from Service */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -100,7 +98,6 @@ class TrackerService: Service(), SensorEventListener {
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener) PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
} }
/* Overrides onStartCommand from Service */ /* Overrides onStartCommand from Service */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -128,7 +125,6 @@ class TrackerService: Service(), SensorEventListener {
return START_STICKY return START_STICKY
} }
/* Overrides onBind from Service */ /* Overrides onBind from Service */
override fun onBind(p0: Intent?): IBinder? { override fun onBind(p0: Intent?): IBinder? {
bound = true bound = true
@ -139,7 +135,6 @@ class TrackerService: Service(), SensorEventListener {
return binder return binder
} }
/* Overrides onRebind from Service */ /* Overrides onRebind from Service */
override fun onRebind(intent: Intent?) { override fun onRebind(intent: Intent?) {
bound = true bound = true
@ -148,7 +143,6 @@ class TrackerService: Service(), SensorEventListener {
addNetworkLocationListener() addNetworkLocationListener()
} }
/* Overrides onUnbind from Service */ /* Overrides onUnbind from Service */
override fun onUnbind(intent: Intent?): Boolean { override fun onUnbind(intent: Intent?): Boolean {
bound = false bound = false
@ -161,7 +155,6 @@ class TrackerService: Service(), SensorEventListener {
return true return true
} }
/* Overrides onDestroy from Service */ /* Overrides onDestroy from Service */
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
@ -180,13 +173,11 @@ class TrackerService: Service(), SensorEventListener {
removeNetworkLocationListener() removeNetworkLocationListener()
} }
/* Overrides onAccuracyChanged from SensorEventListener */ /* Overrides onAccuracyChanged from SensorEventListener */
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
LogHelper.v(TAG, "Accuracy changed: $accuracy") LogHelper.v(TAG, "Accuracy changed: $accuracy")
} }
/* Overrides onSensorChanged from SensorEventListener */ /* Overrides onSensorChanged from SensorEventListener */
override fun onSensorChanged(sensorEvent: SensorEvent?) { override fun onSensorChanged(sensorEvent: SensorEvent?) {
var steps: Float = 0f var steps: Float = 0f
@ -202,7 +193,6 @@ class TrackerService: Service(), SensorEventListener {
track.stepCount = steps track.stepCount = steps
} }
/* 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 not available
@ -220,7 +210,6 @@ class TrackerService: Service(), SensorEventListener {
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 // start receiving location updates
@ -229,8 +218,6 @@ class TrackerService: Service(), SensorEventListener {
// set up new track // set up new track
if (newTrack) { if (newTrack) {
track = Track() track = Track()
track.recordingStart = GregorianCalendar.getInstance().time
track.recordingStop = track.recordingStart
track.name = DateTimeHelper.convertToReadableDate(track.recordingStart) track.name = DateTimeHelper.convertToReadableDate(track.recordingStart)
stepCountOffset = 0f stepCountOffset = 0f
} }
@ -244,7 +231,6 @@ class TrackerService: Service(), SensorEventListener {
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 // save temp track
@ -263,7 +249,6 @@ class TrackerService: Service(), SensorEventListener {
stopForeground(false) stopForeground(false)
} }
/* Clear track recording */ /* Clear track recording */
fun clearTrack() { fun clearTrack() {
track = Track() track = Track()
@ -274,24 +259,6 @@ class TrackerService: Service(), SensorEventListener {
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
} }
// /* Saves track recording to storage */ // todo remove
// fun saveTrack() {
// // save track using "deferred await"
// launch {
// // step 1: create and store filenames for json and gpx files
// track.trackUriString = FileHelper.getTrackFileUri(this@TrackerService, track).toString()
// track.gpxUriString = FileHelper.getGpxFileUri(this@TrackerService, track).toString()
// // step 2: save track
// FileHelper.saveTrackSuspended(track, saveGpxToo = true)
// // step 3: save tracklist
// FileHelper.addTrackAndSaveTracklistSuspended(this@TrackerService, track)
// // step 3: clear track
// clearTrack()
// }
// }
/* Creates location listener */ /* Creates location listener */
private fun createLocationListener(): LocationListener { private fun createLocationListener(): LocationListener {
return object : LocationListener { return object : LocationListener {
@ -331,7 +298,6 @@ class TrackerService: Service(), SensorEventListener {
} }
} }
/* 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 // check if already registered
@ -367,20 +333,34 @@ class TrackerService: Service(), SensorEventListener {
} }
} }
/* Adds a Network location listener to location manager */ /* Adds a Network location listener to location manager */
private fun addNetworkLocationListener() { private fun addNetworkLocationListener() {
// check if already registered if (gpsOnly)
if (!networkLocationListenerRegistered) { {
// check if Network provider is available LogHelper.v(TAG, "User prefers GPS-only.")
return;
}
if (networkLocationListenerRegistered)
{
LogHelper.v(TAG, "Network location listener has already been added.")
return;
}
networkProviderActive = LocationHelper.isNetworkEnabled(locationManager) networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
if (networkProviderActive && !gpsOnly) { if (!networkProviderActive)
// check for location permission {
if (ContextCompat.checkSelfPermission( LogHelper.w(TAG, "Unable to add Network location listener.")
this, return
Manifest.permission.ACCESS_FINE_LOCATION }
) == PackageManager.PERMISSION_GRANTED) {
// adds Network location listener val has_permission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
if (! has_permission)
{
LogHelper.w(TAG, "Unable to add Network location listener. Location permission is not granted.")
return
}
locationManager.requestLocationUpdates( locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER, LocationManager.NETWORK_PROVIDER,
0, 0,
@ -389,23 +369,7 @@ class TrackerService: Service(), SensorEventListener {
) )
networkLocationListenerRegistered = true networkLocationListenerRegistered = true
LogHelper.v(TAG, "Added Network location listener.") LogHelper.v(TAG, "Added Network location listener.")
} else {
LogHelper.w(
TAG,
"Unable to add Network location listener. Location permission is not granted."
)
} }
} else {
LogHelper.w(TAG, "Unable to add Network location listener.")
}
} else {
LogHelper.v(
TAG,
"Skipping registration. Network location listener has already been added."
)
}
}
/* Adds location listeners to location manager */ /* Adds location listeners to location manager */
fun removeGpsLocationListener() { fun removeGpsLocationListener() {
@ -421,7 +385,6 @@ class TrackerService: Service(), SensorEventListener {
} }
} }
/* Adds location listeners to location manager */ /* Adds location listeners to location manager */
fun removeNetworkLocationListener() { fun removeNetworkLocationListener() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
@ -436,7 +399,6 @@ class TrackerService: Service(), SensorEventListener {
} }
} }
/* Registers a step counter listener */ /* Registers a step counter listener */
private fun startStepCounter() { private fun startStepCounter() {
val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI) val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI)
@ -446,12 +408,11 @@ class TrackerService: Service(), SensorEventListener {
} }
} }
/* Displays or updates notification */
/* Displays / updates notification */
private fun displayNotification(): Notification { private fun displayNotification(): Notification {
val notification: Notification = notificationHelper.createNotification( val notification: Notification = notificationHelper.createNotification(
trackingState, trackingState,
track.length, track.distance,
track.duration, track.duration,
useImperial useImperial
) )
@ -459,7 +420,6 @@ class TrackerService: Service(), SensorEventListener {
return notification return notification
} }
/* /*
* Defines the listener for changes in shared preferences * Defines the listener for changes in shared preferences
*/ */
@ -487,7 +447,6 @@ class TrackerService: Service(), SensorEventListener {
* End of declaration * End of declaration
*/ */
/* /*
* Inner class: Local Binder that returns this service * Inner class: Local Binder that returns this service
*/ */
@ -498,7 +457,6 @@ class TrackerService: Service(), SensorEventListener {
* End of inner class * End of inner class
*/ */
/* /*
* Runnable: Periodically track updates (if recording active) * Runnable: Periodically track updates (if recording active)
*/ */
@ -554,7 +512,6 @@ class TrackerService: Service(), SensorEventListener {
* End of declaration * End of declaration
*/ */
/* 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>() {
@ -576,18 +533,4 @@ class TrackerService: Service(), SensorEventListener {
sum = 0.0 sum = 0.0
} }
} }
// // TODO remove
// val testAltitudes: Array<Double> = arrayOf(352.4349365234375, 358.883544921875, 358.6827392578125, 357.31396484375, 354.27459716796875, 354.573486328125, 354.388916015625, 354.6697998046875, 356.534912109375, 355.2772216796875, 356.21246337890625, 352.3499755859375, 350.37646484375, 351.2098388671875, 350.5213623046875, 350.5145263671875, 350.1728515625, 350.9075927734375, 351.5965576171875, 349.55767822265625, 351.548583984375, 357.1195068359375, 362.18634033203125, 366.3153076171875, 366.2218017578125, 362.1046142578125, 357.48291015625, 356.78570556640625, 353.7734375, 352.53936767578125, 351.8125, 353.1099853515625, 354.93035888671875, 355.4337158203125, 354.83270263671875, 352.9859619140625, 352.3006591796875, 351.63470458984375, 350.2501220703125, 351.75726318359375, 350.87664794921875, 350.4185791015625, 350.51568603515625, 349.5537109375, 345.2874755859375, 345.57196044921875, 349.99658203125, 353.3822021484375, 355.19061279296875, 359.1099853515625, 361.74365234375, 363.313232421875, 362.026611328125, 363.20703125, 363.2508544921875, 362.5870361328125, 362.521240234375)
// var testCounter: Int = 0
// fun getTestAltitude(): Double {
// if (testCounter >= testAltitudes.size) testCounter = 0
// val testAltitude: Double = testAltitudes[testCounter]
// testCounter ++
// return testAltitude
// }
// // TODO remove
} }

View File

@ -101,7 +101,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
Keys.ARG_TRACK_TITLE to tracklistElement.name, Keys.ARG_TRACK_TITLE to tracklistElement.name,
Keys.ARG_TRACK_FILE_URI to tracklistElement.trackUriString, Keys.ARG_TRACK_FILE_URI to tracklistElement.trackUriString,
Keys.ARG_GPX_FILE_URI to tracklistElement.gpxUriString, Keys.ARG_GPX_FILE_URI to tracklistElement.gpxUriString,
Keys.ARG_TRACK_ID to TrackHelper.getTrackId(tracklistElement) Keys.ARG_TRACK_ID to tracklistElement.id
) )
findNavController().navigate(R.id.fragment_track, bundle) findNavController().navigate(R.id.fragment_track, bundle)
} }
@ -159,13 +159,15 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
// handle delete request from TrackFragment - after layout calculations are complete // handle delete request from TrackFragment - after layout calculations are complete
val deleteTrackId: Long = arguments?.getLong(Keys.ARG_TRACK_ID, -1L) ?: -1L val deleteTrackId: Long = arguments?.getLong(Keys.ARG_TRACK_ID, -1L) ?: -1L
arguments?.putLong(Keys.ARG_TRACK_ID, -1L) arguments?.putLong(Keys.ARG_TRACK_ID, -1L)
if (deleteTrackId != -1L) { if (deleteTrackId == -1L)
{
return;
}
CoroutineScope(Main). launch { CoroutineScope(Main). launch {
tracklistAdapter.removeTrackById(this@TracklistFragment.activity as Context, deleteTrackId) tracklistAdapter.removeTrackById(this@TracklistFragment.activity as Context, deleteTrackId)
toggleOnboardingLayout() toggleOnboardingLayout()
} }
} }
}
} }
/* /*

View File

@ -14,27 +14,27 @@
* https://github.com/osmdroid/osmdroid * https://github.com/osmdroid/osmdroid
*/ */
package org.y20k.trackbook.core package org.y20k.trackbook.core
import android.content.Context import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.Keep import androidx.annotation.Keep
import com.google.gson.annotations.Expose import com.google.gson.annotations.Expose
import java.util.*
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 java.util.*
/* /*
* Track data class * Track data class
*/ */
@Keep @Keep
@Parcelize @Parcelize
data class Track (@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION, data class Track (@Expose val id: Long = make_random_id(),
@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
@Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(), @Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(),
@Expose var length: Float = 0f, @Expose var distance: Float = 0f,
@Expose var duration: Long = 0L, @Expose var duration: Long = 0L,
@Expose var recordingPaused: Long = 0L, @Expose var recordingPaused: Long = 0L,
@Expose var stepCount: Float = 0f, @Expose var stepCount: Float = 0f,
@ -49,30 +49,27 @@ data class Track (@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMA
@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 = String()): Parcelable
{
/* Creates a TracklistElement */ /* Creates a TracklistElement */
fun toTracklistElement(context: Context): TracklistElement { fun toTracklistElement(context: Context): TracklistElement {
val readableDateString: String = DateTimeHelper.convertToReadableDate(recordingStart) val readableDateString: String = DateTimeHelper.convertToReadableDate(recordingStart)
val readableDurationString: String = DateTimeHelper.convertToReadableTime(context, duration) val readableDurationString: String = DateTimeHelper.convertToReadableTime(context, duration)
return TracklistElement( return TracklistElement(
id = id,
name = name, name = name,
date = recordingStart, date = recordingStart,
dateString = readableDateString, dateString = readableDateString,
length = length, distance = distance,
durationString = readableDurationString, duration = duration,
trackUriString = trackUriString, trackUriString = trackUriString,
gpxUriString = gpxUriString, gpxUriString = gpxUriString,
starred = false starred = false
) )
} }
}
/* Returns unique ID for Track - currently the start date */ fun make_random_id(): Long
fun getTrackId(): Long { {
return recordingStart.time return (Random.nextBits(31).toLong() shl 32) + Random.nextBits(32)
}
} }

View File

@ -32,26 +32,39 @@ import java.util.*
@Keep @Keep
@Parcelize @Parcelize
data class Tracklist (@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION, data class Tracklist (@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION,
@Expose val tracklistElements: MutableList<TracklistElement> = mutableListOf<TracklistElement>(), @Expose val tracklistElements: MutableList<TracklistElement> = mutableListOf<TracklistElement>()): Parcelable {
@Expose var modificationDate: Date = Date(),
@Expose var totalDistanceAll: Float = 0f,
@Expose var totalDurationAll: Long = 0L,
@Expose var totalRecordingPausedAll: Long = 0L,
@Expose var totalStepCountAll: Float = 0f): Parcelable {
/* Return trackelement for given track id */ /* Return trackelement for given track id */
fun getTrackElement(trackId: Long): TracklistElement? { fun getTrackElement(trackId: Long): TracklistElement? {
tracklistElements.forEach { tracklistElement -> tracklistElements.forEach { tracklistElement ->
if (TrackHelper.getTrackId(tracklistElement) == trackId) { if (tracklistElement.id == trackId) {
return tracklistElement return tracklistElement
} }
} }
return null return null
} }
fun get_total_distance(): Float
{
var total: Float = 0F
tracklistElements.forEach { tracklist_element ->
total += tracklist_element.distance
}
return total
}
fun get_total_duration(): Long
{
var total: Long = 0L
tracklistElements.forEach { tracklist_element ->
total += tracklist_element.duration
}
return total
}
/* Create a deep copy */ /* Create a deep copy */
fun deepCopy(): Tracklist { fun deepCopy(): Tracklist {
return Tracklist(tracklistFormatVersion, mutableListOf<TracklistElement>().apply { addAll(tracklistElements) }, modificationDate) return Tracklist(tracklistFormatVersion, mutableListOf<TracklistElement>().apply { addAll(tracklistElements) })
} }
} }

View File

@ -29,18 +29,15 @@ import java.util.*
*/ */
@Keep @Keep
@Parcelize @Parcelize
data class TracklistElement(@Expose var name: String, data class TracklistElement(
@Expose val id: Long,
@Expose var name: String,
@Expose val date: Date, @Expose val date: Date,
@Expose val dateString: String, @Expose val dateString: String,
@Expose val durationString: String, @Expose val duration: Long,
@Expose val length: Float, @Expose val distance: Float,
@Expose val trackUriString: String, @Expose val trackUriString: String,
@Expose val gpxUriString: String, @Expose val gpxUriString: String,
@Expose var starred: Boolean = false): Parcelable { @Expose var starred: Boolean = false
) : Parcelable {
/* Returns unique ID for TracklistElement - currently the start date */
fun getTrackId(): Long {
return date.time
}
} }

View File

@ -84,7 +84,7 @@ object DateTimeHelper {
/* Create sortable string for date - used for filenames */ /* Create sortable string for date - used for filenames */
fun convertToSortableDateString(date: Date): String { fun convertToSortableDateString(date: Date): String {
val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US) val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US)
return dateFormat.format(date) return dateFormat.format(date)
} }

View File

@ -22,6 +22,7 @@ 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.provider.OpenableColumns
import android.util.Log
import androidx.core.net.toFile 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
@ -89,7 +90,6 @@ object FileHelper {
} }
} }
/* Clears given folder - keeps given number of files */ /* Clears given folder - keeps given number of files */
fun clearFolder(folder: File?, keep: Int, deleteFolder: Boolean = false) { fun clearFolder(folder: File?, keep: Int, deleteFolder: Boolean = false) {
if (folder != null && folder.exists()) { if (folder != null && folder.exists()) {
@ -111,15 +111,17 @@ object FileHelper {
/* Reads tracklist from storage using GSON */ /* Reads tracklist from storage using GSON */
fun readTracklist(context: Context): Tracklist { fun readTracklist(context: Context): Tracklist {
LogHelper.v(TAG, "Reading Tracklist - Thread: ${Thread.currentThread().name}") LogHelper.v(TAG, "Reading Tracklist - Thread: ${Thread.currentThread().name}")
// get JSON from text file var folder = context.getExternalFilesDir("tracks")
val json: String = readTextFile(context, getTracklistFileUri(context))
var tracklist: Tracklist = Tracklist() var tracklist: Tracklist = Tracklist()
when (json.isNotBlank()) { Log.i(TAG, folder.toString())
// convert JSON and return as tracklist if (folder != null)
true -> try { {
tracklist = getCustomGson().fromJson(json, Tracklist::class.java) folder.walk().filter{ f: File -> f.isFile }.forEach{ track_file ->
} catch (e: Exception) { val track_json = readTextFile(context, track_file.toUri())
e.printStackTrace() 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 return tracklist
@ -165,7 +167,7 @@ object FileHelper {
/* Creates Uri for json track file */ /* Creates Uri for json track file */
fun getTrackFileUri(context: Context, track: Track): Uri { fun getTrackFileUri(context: Context, track: Track): Uri {
val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.TRACKBOOK_FILE_EXTENSION val fileName: String = track.id.toString() + Keys.TRACKBOOK_FILE_EXTENSION
return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), fileName).toUri() return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), fileName).toUri()
} }
@ -175,21 +177,6 @@ object FileHelper {
return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE).toUri() return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE).toUri()
} }
/* Suspend function: Wrapper for saveTracklist */
suspend fun addTrackAndSaveTracklistSuspended(context: Context, track: Track, modificationDate: Date = track.recordingStop) {
return suspendCoroutine { cont ->
val tracklist: Tracklist = readTracklist(context)
tracklist.tracklistElements.add(track.toTracklistElement(context))
tracklist.totalDistanceAll += track.length
// tracklist.totalDurationAll += track.duration // note: TracklistElement does not contain duration
// tracklist.totalRecordingPausedAll += track.recordingPaused // note: TracklistElement does not contain recordingPaused
// tracklist.totalStepCountAll += track.stepCount // note: TracklistElement does not contain stepCount
cont.resume(saveTracklist(context, tracklist, modificationDate))
}
}
/* 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 ->
@ -198,14 +185,6 @@ object FileHelper {
} }
/* Suspend function: Wrapper for saveTracklist */
suspend fun saveTracklistSuspended(context: Context, tracklist: Tracklist, modificationDate: Date) {
return suspendCoroutine { cont ->
cont.resume(saveTracklist(context, tracklist, modificationDate))
}
}
/* Suspend function: Wrapper for saveTrack */ /* Suspend function: Wrapper for saveTrack */
suspend fun saveTrackSuspended(track: Track, saveGpxToo: Boolean) { suspend fun saveTrackSuspended(track: Track, saveGpxToo: Boolean) {
return suspendCoroutine { cont -> return suspendCoroutine { cont ->
@ -223,13 +202,12 @@ object FileHelper {
/* Suspend function: Wrapper for deleteTrack */ /* Suspend function: Wrapper for deleteTrack */
suspend fun deleteTrackSuspended(context: Context, position: Int, tracklist: Tracklist): Tracklist { suspend fun deleteTrackSuspended(context: Context, tracklist_element: TracklistElement, tracklist: Tracklist): Tracklist {
return suspendCoroutine { cont -> return suspendCoroutine { cont ->
cont.resume(deleteTrack(context, position, tracklist)) cont.resume(deleteTrack(context, tracklist_element, tracklist))
} }
} }
/* Suspend function: Deletes tracks that are not starred using deleteTracks */ /* Suspend function: Deletes tracks that are not starred using deleteTracks */
suspend fun deleteNonStarredSuspended(context: Context, tracklist: Tracklist): Tracklist { suspend fun deleteNonStarredSuspended(context: Context, tracklist: Tracklist): Tracklist {
return suspendCoroutine { cont -> return suspendCoroutine { cont ->
@ -243,7 +221,6 @@ object FileHelper {
} }
} }
/* Suspend function: Wrapper for readTracklist */ /* Suspend function: Wrapper for readTracklist */
suspend fun readTracklistSuspended(context: Context): Tracklist { suspend fun readTracklistSuspended(context: Context): Tracklist {
return suspendCoroutine {cont -> return suspendCoroutine {cont ->
@ -251,7 +228,6 @@ object FileHelper {
} }
} }
/* 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) {
return suspendCoroutine { cont -> return suspendCoroutine { cont ->
@ -259,7 +235,6 @@ object FileHelper {
} }
} }
/* Save Track as JSON to storage */ /* Save Track as JSON to storage */
private fun saveTrack(track: Track, saveGpxToo: Boolean) { private fun saveTrack(track: Track, saveGpxToo: Boolean) {
val jsonString: String = getTrackJsonString(track) val jsonString: String = getTrackJsonString(track)
@ -276,7 +251,6 @@ object FileHelper {
} }
} }
/* Save Temp Track as JSON to storage */ /* Save Temp Track as JSON to storage */
private fun saveTempTrack(context: Context, track: Track) { private fun saveTempTrack(context: Context, track: Track) {
val json: String = getTrackJsonString(track) val json: String = getTrackJsonString(track)
@ -285,47 +259,24 @@ object FileHelper {
} }
} }
/* Saves track tracklist as JSON text file */
private fun saveTracklist(context: Context, tracklist: Tracklist, modificationDate: Date) {
tracklist.modificationDate = modificationDate
// convert to JSON
val gson: Gson = getCustomGson()
var json: String = String()
try {
json = gson.toJson(tracklist)
} catch (e: Exception) {
e.printStackTrace()
}
if (json.isNotBlank()) {
// write text file
writeTextFile(json, getTracklistFileUri(context))
}
}
/* Creates Uri for tracklist file */ /* Creates Uri for tracklist file */
private fun getTracklistFileUri(context: Context): Uri { private fun getTracklistFileUri(context: Context): Uri {
return File(context.getExternalFilesDir(""), Keys.TRACKLIST_FILE).toUri() return File(context.getExternalFilesDir(""), Keys.TRACKLIST_FILE).toUri()
} }
/* Renames track */ /* Renames track */
private fun renameTrack(context: Context, track: Track, newName: String) { private fun renameTrack(context: Context, track: Track, newName: String) {
// search track in tracklist // search track in tracklist
val tracklist: Tracklist = readTracklist(context) val tracklist: Tracklist = readTracklist(context)
var trackUriString: String = String() var trackUriString: String = String()
tracklist.tracklistElements.forEach { tracklistElement -> tracklist.tracklistElements.forEach { tracklistElement ->
if (tracklistElement.getTrackId() == track.getTrackId()) { if (tracklistElement.id == track.id) {
// rename tracklist element // rename tracklist element
tracklistElement.name = newName tracklistElement.name = newName
trackUriString = tracklistElement.trackUriString trackUriString = tracklistElement.trackUriString
} }
} }
if (trackUriString.isNotEmpty()) { if (trackUriString.isNotEmpty()) {
// save tracklist
saveTracklist(context, tracklist, GregorianCalendar.getInstance().time)
// rename track // rename track
track.name = newName track.name = newName
// save track // save track
@ -337,29 +288,27 @@ object FileHelper {
/* Deletes multiple tracks */ /* Deletes multiple tracks */
private fun deleteTracks(context: Context, tracklistElements: MutableList<TracklistElement>, tracklist: Tracklist): Tracklist { private fun deleteTracks(context: Context, tracklistElements: MutableList<TracklistElement>, tracklist: Tracklist): Tracklist {
tracklistElements.forEach { tracklistElement -> tracklistElements.forEach { tracklistElement ->
// delete track files deleteTrack(context, tracklistElement, tracklist)
tracklistElement.trackUriString.toUri().toFile().delete()
tracklistElement.gpxUriString.toUri().toFile().delete()
// subtract track length from total distance
tracklist.totalDistanceAll -= tracklistElement.length
} }
tracklist.tracklistElements.removeAll{ tracklistElements.contains(it) }
saveTracklist(context, tracklist, GregorianCalendar.getInstance().time)
return tracklist return tracklist
} }
/* Deletes one track */ /* Deletes one track */
private fun deleteTrack(context: Context, position: Int, tracklist: Tracklist): Tracklist { private fun deleteTrack(context: Context, tracklist_element: TracklistElement, tracklist: Tracklist): Tracklist {
val tracklistElement: TracklistElement = tracklist.tracklistElements[position]
// delete track files // delete track files
tracklistElement.trackUriString.toUri().toFile().delete() val json_file: File = tracklist_element.trackUriString.toUri().toFile()
tracklistElement.gpxUriString.toUri().toFile().delete() if (json_file.isFile)
// subtract track length from total distance {
tracklist.totalDistanceAll -= tracklistElement.length json_file.delete()
}
val gpx_file: File = tracklist_element.gpxUriString.toUri().toFile()
if (gpx_file.isFile)
{
gpx_file.delete()
}
// remove track element from list // remove track element from list
tracklist.tracklistElements.removeIf { TrackHelper.getTrackId(it) == TrackHelper.getTrackId(tracklistElement) } tracklist.tracklistElements.removeIf {it.id == tracklist_element.id}
saveTracklist(context, tracklist, GregorianCalendar.getInstance().time)
return tracklist return tracklist
} }
@ -389,17 +338,14 @@ object FileHelper {
return json return json
} }
/* Creates a Gson object */ /* Creates a Gson object */
private fun getCustomGson(): Gson { private fun getCustomGson(): Gson {
val gsonBuilder = GsonBuilder() val gsonBuilder = GsonBuilder()
gsonBuilder.setDateFormat("M/d/yy hh:mm a") 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 */ /* Converts byte value into a human readable format */
// Source: https://programming.guide/java/formatting-byte-size-to-human-readable-format.html // Source: https://programming.guide/java/formatting-byte-size-to-human-readable-format.html
fun getReadableByteCount(bytes: Long, si: Boolean = true): String { fun getReadableByteCount(bytes: Long, si: Boolean = true): String {

View File

@ -14,13 +14,14 @@
* https://github.com/osmdroid/osmdroid * https://github.com/osmdroid/osmdroid
*/ */
package org.y20k.trackbook.helpers package org.y20k.trackbook.helpers
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.widget.Toast import android.widget.Toast
import androidx.core.net.toUri import androidx.core.net.toUri
import java.text.SimpleDateFormat
import java.util.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -30,9 +31,6 @@ 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.TracklistElement
import org.y20k.trackbook.core.WayPoint import org.y20k.trackbook.core.WayPoint
import java.text.SimpleDateFormat
import java.util.*
/* /*
* TrackHelper object * TrackHelper object
@ -42,15 +40,6 @@ object TrackHelper {
/* Define log tag */ /* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackHelper::class.java) private val TAG: String = LogHelper.makeLogTag(TrackHelper::class.java)
/* Returns unique ID for Track - currently the start date */
fun getTrackId(track: Track): Long = track.recordingStart.time
/* Returns unique ID for TracklistElement - currently the start date */
fun getTrackId(tracklistElement: TracklistElement): Long = tracklistElement.date.time
/* Adds given locatiom as waypoint to track */ /* Adds given locatiom as waypoint to track */
fun addWayPointToTrack(track: Track, location: Location, accuracyMultiplier: Int, resumed: Boolean): Pair<Boolean, Track> { fun addWayPointToTrack(track: Track, location: Location, accuracyMultiplier: Int, resumed: Boolean): Pair<Boolean, Track> {
// Step 1: Get previous location // Step 1: Get previous location
@ -85,7 +74,7 @@ object TrackHelper {
if (shouldBeAdded) { if (shouldBeAdded) {
// Step 3.1: Update distance (do not update if resumed -> we do not want to add values calculated during a recording pause) // Step 3.1: Update distance (do not update if resumed -> we do not want to add values calculated during a recording pause)
if (!resumed) { if (!resumed) {
track.length = track.length + LocationHelper.calculateDistance(previousLocation, location) track.distance = track.distance + LocationHelper.calculateDistance(previousLocation, location)
} }
// Step 3.2: Update altitude values // Step 3.2: Update altitude values
val altitude: Double = location.altitude val altitude: Double = location.altitude
@ -109,17 +98,15 @@ object TrackHelper {
track.longitude = location.longitude track.longitude = location.longitude
// Step 3.5: Add location as new waypoint // Step 3.5: Add location as new waypoint
track.wayPoints.add(WayPoint(location = location, distanceToStartingPoint = track.length)) track.wayPoints.add(WayPoint(location = location, distanceToStartingPoint = track.distance))
} }
return Pair(shouldBeAdded, track) 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 */ /* Creates GPX string for given track */
fun createGpxString(track: Track): String { fun createGpxString(track: Track): String {
var gpxString: String var gpxString: String
@ -236,7 +223,6 @@ object TrackHelper {
return gpxTrack.toString() return gpxTrack.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 toggleStarred(context: Context, track: Track, latitude: Double, longitude: Double): Track {
track.wayPoints.forEach { waypoint -> track.wayPoints.forEach { waypoint ->
@ -250,28 +236,4 @@ object TrackHelper {
} }
return track return track
} }
/* Calculates total distance, duration and pause */
fun calculateAndSaveTrackTotals(context: Context, tracklist: Tracklist) {
CoroutineScope(IO).launch {
var totalDistanceAll: Float = 0f
// var totalDurationAll: Long = 0L
// var totalRecordingPausedAll: Long = 0L
// var totalStepCountAll: Float = 0f
tracklist.tracklistElements.forEach { tracklistElement ->
val track: Track = FileHelper.readTrack(context, tracklistElement.trackUriString.toUri())
totalDistanceAll += track.length
// totalDurationAll += track.duration
// totalRecordingPausedAll += track.recordingPaused
// totalStepCountAll += track.stepCount
}
tracklist.totalDistanceAll = totalDistanceAll
// tracklist.totalDurationAll = totalDurationAll
// tracklist.totalRecordingPausedAll = totalRecordingPausedAll
// tracklist.totalStepCountAll = totalStepCountAll
FileHelper.saveTracklistSuspended(context, tracklist, GregorianCalendar.getInstance().time)
}
}
} }

View File

@ -19,6 +19,7 @@ package org.y20k.trackbook.tracklist
import android.content.Context import android.content.Context
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -29,6 +30,7 @@ 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 kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -37,7 +39,6 @@ import org.y20k.trackbook.R
import org.y20k.trackbook.core.Tracklist import org.y20k.trackbook.core.Tracklist
import org.y20k.trackbook.core.TracklistElement import org.y20k.trackbook.core.TracklistElement
import org.y20k.trackbook.helpers.* import org.y20k.trackbook.helpers.*
import java.util.*
/* /*
@ -70,10 +71,6 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
// load tracklist // load tracklist
tracklist = FileHelper.readTracklist(context) tracklist = FileHelper.readTracklist(context)
tracklist.tracklistElements.sortByDescending { tracklistElement -> tracklistElement.date } tracklist.tracklistElements.sortByDescending { tracklistElement -> tracklistElement.date }
// calculate total duration and distance, if necessary
if (tracklist.tracklistElements.isNotEmpty() && tracklist.totalDurationAll == 0L) {
TrackHelper.calculateAndSaveTrackTotals(context, tracklist)
}
} }
@ -118,12 +115,12 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
// CASE STATISTICS ELEMENT // CASE STATISTICS ELEMENT
is ElementStatisticsViewHolder -> { is ElementStatisticsViewHolder -> {
val elementStatisticsViewHolder: ElementStatisticsViewHolder = holder as ElementStatisticsViewHolder val elementStatisticsViewHolder: ElementStatisticsViewHolder = holder as ElementStatisticsViewHolder
elementStatisticsViewHolder.totalDistanceView.text = LengthUnitHelper.convertDistanceToString(tracklist.totalDistanceAll, useImperial) elementStatisticsViewHolder.totalDistanceView.text = LengthUnitHelper.convertDistanceToString(tracklist.get_total_distance(), useImperial)
} }
// CASE TRACK ELEMENT // CASE TRACK ELEMENT
is ElementTrackViewHolder -> { is ElementTrackViewHolder -> {
val positionInTracklist: Int = position -1 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.tracklistElements[positionInTracklist].name
elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist) elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist)
@ -154,8 +151,9 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/* Removes track and track files for given position - used by TracklistFragment */ /* Removes track and track files for given position - used by TracklistFragment */
fun removeTrackAtPosition(context: Context, position: Int) { fun removeTrackAtPosition(context: Context, position: Int) {
CoroutineScope(IO).launch { CoroutineScope(IO).launch {
val positionInTracklist = position - 1 val index = position - 1 // position 0 is the statistics element
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, positionInTracklist, tracklist) } val tracklist_element = tracklist.tracklistElements[index]
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, tracklist_element, tracklist) }
// wait for result and store in tracklist // wait for result and store in tracklist
withContext(Main) { withContext(Main) {
tracklist = deferred.await() tracklist = deferred.await()
@ -166,20 +164,23 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
} }
} }
/* Removes track and track files for given track id - used by TracklistFragment */ /* Removes track and track files for given track id - used by TracklistFragment */
fun removeTrackById(context: Context, trackId: Long) { fun removeTrackById(context: Context, trackId: Long) {
CoroutineScope(IO).launch { CoroutineScope(IO).launch {
// reload tracklist //todo check if necessary // reload tracklist //todo check if necessary
// tracklist = FileHelper.readTracklist(context) tracklist = FileHelper.readTracklist(context)
val positionInTracklist: Int = findPosition(trackId) val index: Int = tracklist.tracklistElements.indexOfFirst {it.id == trackId}
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, positionInTracklist, tracklist) } if (index == -1) {
return@launch
}
val tracklist_element = tracklist.tracklistElements[index]
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, tracklist_element, tracklist) }
// wait for result and store in tracklist // wait for result and store in tracklist
val position = index + 1 // position 0 is the statistics element
withContext(Main) { withContext(Main) {
tracklist = deferred.await() tracklist = deferred.await()
val positionInRecyclerView: Int = positionInTracklist + 1 // position 0 is the statistics element
notifyItemChanged(0) notifyItemChanged(0)
notifyItemRemoved(positionInRecyclerView) notifyItemRemoved(position)
notifyItemRangeChanged(position, tracklist.tracklistElements.size) notifyItemRangeChanged(position, tracklist.tracklistElements.size)
} }
} }
@ -195,7 +196,10 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/* Finds current position of track element in adapter list */ /* Finds current position of track element in adapter list */
private fun findPosition(trackId: Long): Int { private fun findPosition(trackId: Long): Int {
tracklist.tracklistElements.forEachIndexed {index, tracklistElement -> tracklist.tracklistElements.forEachIndexed {index, tracklistElement ->
if (tracklistElement.getTrackId() == trackId) return index if (tracklistElement.id == trackId)
{
return index
}
} }
return -1 return -1
} }
@ -214,21 +218,19 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
tracklist.tracklistElements[position].starred = true tracklist.tracklistElements[position].starred = true
} }
} }
CoroutineScope(Dispatchers.IO).launch {
FileHelper.saveTracklistSuspended(context, tracklist, GregorianCalendar.getInstance().time)
}
} }
/* 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 tracklistElement: TracklistElement = tracklist.tracklistElements[position]
val track_duration_string = DateTimeHelper.convertToReadableTime(context, tracklistElement.duration)
val trackDataString: String val trackDataString: String
when (tracklistElement.name == tracklistElement.dateString) { when (tracklistElement.name == tracklistElement.dateString) {
// CASE: no individual name set - exclude date // CASE: no individual name set - exclude date
true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(tracklistElement.length, useImperial)}${tracklistElement.durationString}" true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(tracklistElement.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.length, useImperial)}${tracklistElement.durationString}" false -> trackDataString = "${tracklistElement.dateString}${LengthUnitHelper.convertDistanceToString(tracklistElement.distance, useImperial)}${track_duration_string}"
} }
return trackDataString return trackDataString
} }
@ -242,7 +244,7 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList.tracklistElements[oldItemPosition] val oldItem = oldList.tracklistElements[oldItemPosition]
val newItem = newList.tracklistElements[newItemPosition] val newItem = newList.tracklistElements[newItemPosition]
return TrackHelper.getTrackId(oldItem) == TrackHelper.getTrackId(newItem) return oldItem.id == newItem.id
} }
override fun getOldListSize(): Int { override fun getOldListSize(): Int {
@ -256,7 +258,7 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList.tracklistElements[oldItemPosition] val oldItem = oldList.tracklistElements[oldItemPosition]
val newItem = newList.tracklistElements[newItemPosition] val newItem = newList.tracklistElements[newItemPosition]
return TrackHelper.getTrackId(oldItem) == TrackHelper.getTrackId(newItem) && oldItem.length == newItem.length return (oldItem.id == newItem.id) && (oldItem.distance == newItem.distance)
} }
} }
/* /*

View File

@ -203,13 +203,13 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
/* Update live statics */ /* Update live statics */
fun updateLiveStatics(length: Float, duration: Long, trackingState: Int) { fun updateLiveStatics(distance: Float, duration: Long, trackingState: Int) {
// toggle visibility // toggle visibility
val trackingActive: Boolean = trackingState != Keys.STATE_TRACKING_NOT_STARTED val trackingActive: Boolean = trackingState != Keys.STATE_TRACKING_NOT_STARTED
liveStatisticsDistanceView.isVisible = trackingActive liveStatisticsDistanceView.isVisible = trackingActive
liveStatisticsDurationView.isVisible = trackingActive liveStatisticsDurationView.isVisible = trackingActive
// update distance and duration (and add outline) // update distance and duration (and add outline)
val distanceString: String = LengthUnitHelper.convertDistanceToString(length, useImperial) val distanceString: String = LengthUnitHelper.convertDistanceToString(distance, useImperial)
liveStatisticsDistanceView.text = distanceString liveStatisticsDistanceView.text = distanceString
liveStatisticsDistanceOutlineView.text = distanceString liveStatisticsDistanceOutlineView.text = distanceString
liveStatisticsDistanceOutlineView.paint.strokeWidth = 5f liveStatisticsDistanceOutlineView.paint.strokeWidth = 5f

View File

@ -78,6 +78,7 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
private val statisticsSheetBehavior: BottomSheetBehavior<View> private val statisticsSheetBehavior: BottomSheetBehavior<View>
private val statisticsSheet: NestedScrollView private val statisticsSheet: NestedScrollView
private val statisticsView: View private val statisticsView: View
private val trackidView: MaterialTextView
private val distanceView: MaterialTextView private val distanceView: MaterialTextView
private val stepsTitleView: MaterialTextView private val stepsTitleView: MaterialTextView
private val stepsView: MaterialTextView private val stepsView: MaterialTextView
@ -120,6 +121,7 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
// get views for statistics sheet // get views for statistics sheet
statisticsSheet = rootView.findViewById(R.id.statistics_sheet) statisticsSheet = rootView.findViewById(R.id.statistics_sheet)
statisticsView = rootView.findViewById(R.id.statistics_view) statisticsView = rootView.findViewById(R.id.statistics_view)
trackidView = rootView.findViewById(R.id.statistics_data_trackid)
distanceView = rootView.findViewById(R.id.statistics_data_distance) distanceView = rootView.findViewById(R.id.statistics_data_distance)
stepsTitleView = rootView.findViewById(R.id.statistics_p_steps) stepsTitleView = rootView.findViewById(R.id.statistics_p_steps)
stepsView = rootView.findViewById(R.id.statistics_data_steps) stepsView = rootView.findViewById(R.id.statistics_data_steps)
@ -190,8 +192,10 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
/* Saves zoom level and center of this map */ /* Saves zoom level and center of this map */
fun saveViewStateToTrack() { fun saveViewStateToTrack()
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 { FileHelper.saveTrackSuspended(track, false) }
} }
} }
@ -202,12 +206,14 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
// get step count string - hide step count if not available // get step count string - hide step count if not available
val steps: String val steps: String
if (track.stepCount == -1f) { if (track.stepCount == -1f)
{
steps = context.getString(R.string.statistics_sheet_p_steps_no_pedometer) steps = context.getString(R.string.statistics_sheet_p_steps_no_pedometer)
stepsTitleView.isGone = true stepsTitleView.isGone = true
stepsView.isGone = true stepsView.isGone = true
} }
else { else
{
steps = track.stepCount.roundToInt().toString() steps = track.stepCount.roundToInt().toString()
stepsTitleView.isVisible = true stepsTitleView.isVisible = true
stepsView.isVisible = true stepsView.isVisible = true
@ -215,11 +221,12 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
// populate views // populate views
trackNameView.text = track.name trackNameView.text = track.name
distanceView.text = LengthUnitHelper.convertDistanceToString(track.length, useImperialUnits) trackidView.text = track.id.toString()
distanceView.text = LengthUnitHelper.convertDistanceToString(track.distance, useImperialUnits)
stepsView.text = steps stepsView.text = steps
waypointsView.text = track.wayPoints.size.toString() waypointsView.text = track.wayPoints.size.toString()
durationView.text = DateTimeHelper.convertToReadableTime(context, track.duration) durationView.text = DateTimeHelper.convertToReadableTime(context, track.duration)
velocityView.text = LengthUnitHelper.convertToVelocityString(track.duration, track.recordingPaused, track.length, useImperialUnits) velocityView.text = LengthUnitHelper.convertToVelocityString(track.duration, track.recordingPaused, track.distance, useImperialUnits)
recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStart) recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStart)
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStop) recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStop)
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits) maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits)

View File

@ -75,17 +75,41 @@
app:srcCompat="@drawable/ic_save_to_storage_24dp" /> app:srcCompat="@drawable/ic_save_to_storage_24dp" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/statistics_p_distance" android:id="@+id/statistics_p_trackid"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:text="@string/statistics_sheet_p_distance" android:text="@string/statistics_sheet_p_trackid"
android:textAllCaps="false" android:textAllCaps="false"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/text_lightweight" android:textColor="@color/text_lightweight"
app:layout_constraintStart_toStartOf="@+id/statistics_track_name_headline" app:layout_constraintStart_toStartOf="@+id/statistics_track_name_headline"
app:layout_constraintTop_toBottomOf="@+id/statistics_track_name_headline" /> app:layout_constraintTop_toBottomOf="@+id/statistics_track_name_headline" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/statistics_data_trackid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="@color/text_default"
app:layout_constraintBottom_toBottomOf="@+id/statistics_p_trackid"
app:layout_constraintStart_toEndOf="@+id/statistics_p_trackid"
app:layout_constraintTop_toTopOf="@+id/statistics_p_trackid"
tools:text="@string/sample_text_default_data" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/statistics_p_distance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/statistics_sheet_p_distance"
android:textAllCaps="false"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/text_lightweight"
app:layout_constraintStart_toStartOf="@+id/statistics_track_name_headline"
app:layout_constraintTop_toBottomOf="@+id/statistics_p_trackid" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/statistics_data_distance" android:id="@+id/statistics_data_distance"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -52,6 +52,7 @@
<string name="marker_description_time">Time</string> <string name="marker_description_time">Time</string>
<string name="marker_description_accuracy">Accuracy</string> <string name="marker_description_accuracy">Accuracy</string>
<!-- Statistics Sheet --> <!-- Statistics Sheet -->
<string name="statistics_sheet_p_trackid">Track ID:</string>
<string name="statistics_sheet_p_distance">Total distance:</string> <string name="statistics_sheet_p_distance">Total distance:</string>
<string name="statistics_sheet_p_steps">Steps taken:</string> <string name="statistics_sheet_p_steps">Steps taken:</string>
<string name="statistics_sheet_p_steps_no_pedometer">pedometer not available</string> <string name="statistics_sheet_p_steps_no_pedometer">pedometer not available</string>