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 */
private fun saveTrack() {
private fun saveTrack()
{
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
{
@ -288,8 +294,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
track.gpxUriString = FileHelper.getGpxFileUri(activity as Context, track).toString()
// step 2: save track
FileHelper.saveTrackSuspended(track, saveGpxToo = true)
// step 3: save tracklist - suspended
FileHelper.addTrackAndSaveTracklistSuspended(activity as Context, track)
// step 3: clear track
trackerService.clearTrack()
// 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_FILE_URI, tracklistElement.trackUriString)
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)
}
@ -372,7 +376,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
// update location and track
layout.markCurrentPosition(currentBestLocation, 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
if (!layout.userInteraction) { layout.centerMap(currentBestLocation, true)}
// show error snackbar if necessary

View File

@ -90,7 +90,12 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
// set up delete button
layout.deleteButton.setOnClickListener {
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
layout.editButton.setOnClickListener {
@ -154,7 +159,7 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
// user tapped remove track
true -> {
// 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)
}
}

View File

@ -14,17 +14,15 @@
* https://github.com/osmdroid/osmdroid
*/
package org.y20k.trackbook
import android.Manifest
import android.app.Notification
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.SharedPreferences
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
@ -32,16 +30,18 @@ import android.hardware.SensorManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.Manifest
import android.os.*
import android.util.Log
import androidx.core.content.ContextCompat
import java.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.launch
import kotlinx.coroutines.Runnable
import org.y20k.trackbook.core.Track
import org.y20k.trackbook.helpers.*
import java.util.*
/*
* TrackerService class
@ -51,7 +51,6 @@ class TrackerService: Service(), SensorEventListener {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackerService::class.java)
/* Main class variables */
var trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED
var gpsProviderActive: Boolean = false
@ -77,7 +76,6 @@ class TrackerService: Service(), SensorEventListener {
private lateinit var gpsLocationListener: LocationListener
private lateinit var networkLocationListener: LocationListener
/* Overrides onCreate from Service */
override fun onCreate() {
super.onCreate()
@ -100,7 +98,6 @@ class TrackerService: Service(), SensorEventListener {
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
}
/* Overrides onStartCommand from Service */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -128,7 +125,6 @@ class TrackerService: Service(), SensorEventListener {
return START_STICKY
}
/* Overrides onBind from Service */
override fun onBind(p0: Intent?): IBinder? {
bound = true
@ -139,7 +135,6 @@ class TrackerService: Service(), SensorEventListener {
return binder
}
/* Overrides onRebind from Service */
override fun onRebind(intent: Intent?) {
bound = true
@ -148,7 +143,6 @@ class TrackerService: Service(), SensorEventListener {
addNetworkLocationListener()
}
/* Overrides onUnbind from Service */
override fun onUnbind(intent: Intent?): Boolean {
bound = false
@ -161,7 +155,6 @@ class TrackerService: Service(), SensorEventListener {
return true
}
/* Overrides onDestroy from Service */
override fun onDestroy() {
super.onDestroy()
@ -180,13 +173,11 @@ class TrackerService: Service(), SensorEventListener {
removeNetworkLocationListener()
}
/* Overrides onAccuracyChanged from SensorEventListener */
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
LogHelper.v(TAG, "Accuracy changed: $accuracy")
}
/* Overrides onSensorChanged from SensorEventListener */
override fun onSensorChanged(sensorEvent: SensorEvent?) {
var steps: Float = 0f
@ -202,7 +193,6 @@ class TrackerService: Service(), SensorEventListener {
track.stepCount = steps
}
/* Resume tracking after stop/pause */
fun resumeTracking() {
// load temp track - returns an empty track if not available
@ -220,7 +210,6 @@ class TrackerService: Service(), SensorEventListener {
startTracking(newTrack = false)
}
/* Start tracking location */
fun startTracking(newTrack: Boolean = true) {
// start receiving location updates
@ -229,8 +218,6 @@ class TrackerService: Service(), SensorEventListener {
// set up new track
if (newTrack) {
track = Track()
track.recordingStart = GregorianCalendar.getInstance().time
track.recordingStop = track.recordingStart
track.name = DateTimeHelper.convertToReadableDate(track.recordingStart)
stepCountOffset = 0f
}
@ -244,7 +231,6 @@ class TrackerService: Service(), SensorEventListener {
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
}
/* Stop tracking location */
fun stopTracking() {
// save temp track
@ -263,7 +249,6 @@ class TrackerService: Service(), SensorEventListener {
stopForeground(false)
}
/* Clear track recording */
fun clearTrack() {
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
}
// /* 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 */
private fun createLocationListener(): LocationListener {
return object : LocationListener {
@ -331,7 +298,6 @@ class TrackerService: Service(), SensorEventListener {
}
}
/* Adds a GPS location listener to location manager */
private fun addGpsLocationListener() {
// check if already registered
@ -367,20 +333,34 @@ class TrackerService: Service(), SensorEventListener {
}
}
/* Adds a Network location listener to location manager */
private fun addNetworkLocationListener() {
// check if already registered
if (!networkLocationListenerRegistered) {
// check if Network provider is available
if (gpsOnly)
{
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)
if (networkProviderActive && !gpsOnly) {
// check for location permission
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED) {
// adds Network location listener
if (!networkProviderActive)
{
LogHelper.w(TAG, "Unable to add Network location listener.")
return
}
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.NETWORK_PROVIDER,
0,
@ -389,23 +369,7 @@ class TrackerService: Service(), SensorEventListener {
)
networkLocationListenerRegistered = true
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 */
fun removeGpsLocationListener() {
@ -421,7 +385,6 @@ class TrackerService: Service(), SensorEventListener {
}
}
/* Adds location listeners to location manager */
fun removeNetworkLocationListener() {
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 */
private fun startStepCounter() {
val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI)
@ -446,12 +408,11 @@ class TrackerService: Service(), SensorEventListener {
}
}
/* Displays / updates notification */
/* Displays or updates notification */
private fun displayNotification(): Notification {
val notification: Notification = notificationHelper.createNotification(
trackingState,
track.length,
track.distance,
track.duration,
useImperial
)
@ -459,7 +420,6 @@ class TrackerService: Service(), SensorEventListener {
return notification
}
/*
* Defines the listener for changes in shared preferences
*/
@ -487,7 +447,6 @@ class TrackerService: Service(), SensorEventListener {
* End of declaration
*/
/*
* Inner class: Local Binder that returns this service
*/
@ -498,7 +457,6 @@ class TrackerService: Service(), SensorEventListener {
* End of inner class
*/
/*
* Runnable: Periodically track updates (if recording active)
*/
@ -554,7 +512,6 @@ class TrackerService: Service(), SensorEventListener {
* End of declaration
*/
/* Simple queue that evicts older elements and holds an average */
/* Credit: CircularQueue https://stackoverflow.com/a/51923797 */
class SimpleMovingAverageQueue(var capacity: Int) : LinkedList<Double>() {
@ -576,18 +533,4 @@ class TrackerService: Service(), SensorEventListener {
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_FILE_URI to tracklistElement.trackUriString,
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)
}
@ -159,13 +159,15 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
// handle delete request from TrackFragment - after layout calculations are complete
val deleteTrackId: Long = arguments?.getLong(Keys.ARG_TRACK_ID, -1L) ?: -1L
arguments?.putLong(Keys.ARG_TRACK_ID, -1L)
if (deleteTrackId != -1L) {
if (deleteTrackId == -1L)
{
return;
}
CoroutineScope(Main). launch {
tracklistAdapter.removeTrackById(this@TracklistFragment.activity as Context, deleteTrackId)
toggleOnboardingLayout()
}
}
}
}
/*

View File

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

View File

@ -32,26 +32,39 @@ import java.util.*
@Keep
@Parcelize
data class Tracklist (@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION,
@Expose val tracklistElements: MutableList<TracklistElement> = mutableListOf<TracklistElement>(),
@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 {
@Expose val tracklistElements: MutableList<TracklistElement> = mutableListOf<TracklistElement>()): Parcelable {
/* Return trackelement for given track id */
fun getTrackElement(trackId: Long): TracklistElement? {
tracklistElements.forEach { tracklistElement ->
if (TrackHelper.getTrackId(tracklistElement) == trackId) {
if (tracklistElement.id == trackId) {
return tracklistElement
}
}
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 */
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
@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 dateString: String,
@Expose val durationString: String,
@Expose val length: Float,
@Expose val duration: Long,
@Expose val distance: Float,
@Expose val trackUriString: String,
@Expose val gpxUriString: String,
@Expose var starred: Boolean = false): Parcelable {
/* Returns unique ID for TracklistElement - currently the start date */
fun getTrackId(): Long {
return date.time
}
@Expose var starred: Boolean = false
) : Parcelable {
}

View File

@ -84,7 +84,7 @@ object DateTimeHelper {
/* Create sortable string for date - used for filenames */
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)
}

View File

@ -22,6 +22,7 @@ import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.google.gson.Gson
@ -89,7 +90,6 @@ object FileHelper {
}
}
/* Clears given folder - keeps given number of files */
fun clearFolder(folder: File?, keep: Int, deleteFolder: Boolean = false) {
if (folder != null && folder.exists()) {
@ -111,15 +111,17 @@ object FileHelper {
/* Reads tracklist from storage using GSON */
fun readTracklist(context: Context): Tracklist {
LogHelper.v(TAG, "Reading Tracklist - Thread: ${Thread.currentThread().name}")
// get JSON from text file
val json: String = readTextFile(context, getTracklistFileUri(context))
var folder = context.getExternalFilesDir("tracks")
var tracklist: Tracklist = Tracklist()
when (json.isNotBlank()) {
// convert JSON and return as tracklist
true -> try {
tracklist = getCustomGson().fromJson(json, Tracklist::class.java)
} catch (e: Exception) {
e.printStackTrace()
Log.i(TAG, folder.toString())
if (folder != null)
{
folder.walk().filter{ f: File -> f.isFile }.forEach{ track_file ->
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
@ -165,7 +167,7 @@ object FileHelper {
/* Creates Uri for json track file */
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()
}
@ -175,21 +177,6 @@ object FileHelper {
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 fun renameTrackSuspended(context: Context, track: Track, newName: String) {
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 fun saveTrackSuspended(track: Track, saveGpxToo: Boolean) {
return suspendCoroutine { cont ->
@ -223,13 +202,12 @@ object FileHelper {
/* 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 ->
cont.resume(deleteTrack(context, position, tracklist))
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 ->
@ -243,7 +221,6 @@ object FileHelper {
}
}
/* Suspend function: Wrapper for readTracklist */
suspend fun readTracklistSuspended(context: Context): Tracklist {
return suspendCoroutine {cont ->
@ -251,7 +228,6 @@ object FileHelper {
}
}
/* Suspend function: Wrapper for copyFile */
suspend fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
return suspendCoroutine { cont ->
@ -259,7 +235,6 @@ object FileHelper {
}
}
/* Save Track as JSON to storage */
private fun saveTrack(track: Track, saveGpxToo: Boolean) {
val jsonString: String = getTrackJsonString(track)
@ -276,7 +251,6 @@ object FileHelper {
}
}
/* Save Temp Track as JSON to storage */
private fun saveTempTrack(context: Context, track: 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 */
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.getTrackId() == track.getTrackId()) {
if (tracklistElement.id == track.id) {
// rename tracklist element
tracklistElement.name = newName
trackUriString = tracklistElement.trackUriString
}
}
if (trackUriString.isNotEmpty()) {
// save tracklist
saveTracklist(context, tracklist, GregorianCalendar.getInstance().time)
// rename track
track.name = newName
// save track
@ -337,29 +288,27 @@ object FileHelper {
/* Deletes multiple tracks */
private fun deleteTracks(context: Context, tracklistElements: MutableList<TracklistElement>, tracklist: Tracklist): Tracklist {
tracklistElements.forEach { tracklistElement ->
// delete track files
tracklistElement.trackUriString.toUri().toFile().delete()
tracklistElement.gpxUriString.toUri().toFile().delete()
// subtract track length from total distance
tracklist.totalDistanceAll -= tracklistElement.length
deleteTrack(context, tracklistElement, tracklist)
}
tracklist.tracklistElements.removeAll{ tracklistElements.contains(it) }
saveTracklist(context, tracklist, GregorianCalendar.getInstance().time)
return tracklist
}
/* Deletes one track */
private fun deleteTrack(context: Context, position: Int, tracklist: Tracklist): Tracklist {
val tracklistElement: TracklistElement = tracklist.tracklistElements[position]
private fun deleteTrack(context: Context, tracklist_element: TracklistElement, tracklist: Tracklist): Tracklist {
// delete track files
tracklistElement.trackUriString.toUri().toFile().delete()
tracklistElement.gpxUriString.toUri().toFile().delete()
// subtract track length from total distance
tracklist.totalDistanceAll -= tracklistElement.length
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 { TrackHelper.getTrackId(it) == TrackHelper.getTrackId(tracklistElement) }
saveTracklist(context, tracklist, GregorianCalendar.getInstance().time)
tracklist.tracklistElements.removeIf {it.id == tracklist_element.id}
return tracklist
}
@ -389,17 +338,14 @@ object FileHelper {
return json
}
/* Creates a Gson object */
private fun getCustomGson(): Gson {
val gsonBuilder = GsonBuilder()
gsonBuilder.setDateFormat("M/d/yy hh:mm a")
gsonBuilder.setDateFormat("yyyy-MM-dd-HH-mm-ss")
gsonBuilder.excludeFieldsWithoutExposeAnnotation()
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 {

View File

@ -14,13 +14,14 @@
* https://github.com/osmdroid/osmdroid
*/
package org.y20k.trackbook.helpers
import android.content.Context
import android.location.Location
import android.widget.Toast
import androidx.core.net.toUri
import java.text.SimpleDateFormat
import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
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.TracklistElement
import org.y20k.trackbook.core.WayPoint
import java.text.SimpleDateFormat
import java.util.*
/*
* TrackHelper object
@ -42,15 +40,6 @@ object TrackHelper {
/* Define log tag */
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 */
fun addWayPointToTrack(track: Track, location: Location, accuracyMultiplier: Int, resumed: Boolean): Pair<Boolean, Track> {
// Step 1: Get previous location
@ -85,7 +74,7 @@ object TrackHelper {
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.length = track.length + LocationHelper.calculateDistance(previousLocation, location)
track.distance = track.distance + LocationHelper.calculateDistance(previousLocation, location)
}
// Step 3.2: Update altitude values
val altitude: Double = location.altitude
@ -109,17 +98,15 @@ object TrackHelper {
track.longitude = location.longitude
// 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)
}
/* Calculates time passed since last stop of recording */
fun calculateDurationOfPause(recordingStop: Date): Long = GregorianCalendar.getInstance().time.time - recordingStop.time
/* Creates GPX string for given track */
fun createGpxString(track: Track): String {
var gpxString: String
@ -236,7 +223,6 @@ object TrackHelper {
return gpxTrack.toString()
}
/* Toggles starred flag for given position */
fun toggleStarred(context: Context, track: Track, latitude: Double, longitude: Double): Track {
track.wayPoints.forEach { waypoint ->
@ -250,28 +236,4 @@ object TrackHelper {
}
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.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -29,6 +30,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import java.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
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.TracklistElement
import org.y20k.trackbook.helpers.*
import java.util.*
/*
@ -70,10 +71,6 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
// load tracklist
tracklist = FileHelper.readTracklist(context)
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
is 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
is ElementTrackViewHolder -> {
val positionInTracklist: Int = position -1
val positionInTracklist: Int = position - 1 // Element 0 is the statistics element.
val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder
elementTrackViewHolder.trackNameView.text = tracklist.tracklistElements[positionInTracklist].name
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 */
fun removeTrackAtPosition(context: Context, position: Int) {
CoroutineScope(IO).launch {
val positionInTracklist = position - 1
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, positionInTracklist, tracklist) }
val index = position - 1 // position 0 is the statistics element
val tracklist_element = tracklist.tracklistElements[index]
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, tracklist_element, tracklist) }
// wait for result and store in tracklist
withContext(Main) {
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 */
fun removeTrackById(context: Context, trackId: Long) {
CoroutineScope(IO).launch {
// reload tracklist //todo check if necessary
// tracklist = FileHelper.readTracklist(context)
val positionInTracklist: Int = findPosition(trackId)
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, positionInTracklist, tracklist) }
tracklist = FileHelper.readTracklist(context)
val index: Int = tracklist.tracklistElements.indexOfFirst {it.id == trackId}
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
val position = index + 1 // position 0 is the statistics element
withContext(Main) {
tracklist = deferred.await()
val positionInRecyclerView: Int = positionInTracklist + 1 // position 0 is the statistics element
notifyItemChanged(0)
notifyItemRemoved(positionInRecyclerView)
notifyItemRemoved(position)
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 */
private fun findPosition(trackId: Long): Int {
tracklist.tracklistElements.forEachIndexed {index, tracklistElement ->
if (tracklistElement.getTrackId() == trackId) return index
if (tracklistElement.id == trackId)
{
return index
}
}
return -1
}
@ -214,21 +218,19 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
tracklist.tracklistElements[position].starred = true
}
}
CoroutineScope(Dispatchers.IO).launch {
FileHelper.saveTracklistSuspended(context, tracklist, GregorianCalendar.getInstance().time)
}
}
/* Creates the track data string */
private fun createTrackDataString(position: Int): String {
val tracklistElement: TracklistElement = tracklist.tracklistElements[position]
val track_duration_string = DateTimeHelper.convertToReadableTime(context, tracklistElement.duration)
val trackDataString: String
when (tracklistElement.name == tracklistElement.dateString) {
// 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
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
}
@ -242,7 +244,7 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList.tracklistElements[oldItemPosition]
val newItem = newList.tracklistElements[newItemPosition]
return TrackHelper.getTrackId(oldItem) == TrackHelper.getTrackId(newItem)
return oldItem.id == newItem.id
}
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 {
val oldItem = oldList.tracklistElements[oldItemPosition]
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 */
fun updateLiveStatics(length: Float, duration: Long, trackingState: Int) {
fun updateLiveStatics(distance: Float, duration: Long, trackingState: Int) {
// toggle visibility
val trackingActive: Boolean = trackingState != Keys.STATE_TRACKING_NOT_STARTED
liveStatisticsDistanceView.isVisible = trackingActive
liveStatisticsDurationView.isVisible = trackingActive
// update distance and duration (and add outline)
val distanceString: String = LengthUnitHelper.convertDistanceToString(length, useImperial)
val distanceString: String = LengthUnitHelper.convertDistanceToString(distance, useImperial)
liveStatisticsDistanceView.text = distanceString
liveStatisticsDistanceOutlineView.text = distanceString
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 statisticsSheet: NestedScrollView
private val statisticsView: View
private val trackidView: MaterialTextView
private val distanceView: MaterialTextView
private val stepsTitleView: MaterialTextView
private val stepsView: MaterialTextView
@ -120,6 +121,7 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
// get views for statistics sheet
statisticsSheet = rootView.findViewById(R.id.statistics_sheet)
statisticsView = rootView.findViewById(R.id.statistics_view)
trackidView = rootView.findViewById(R.id.statistics_data_trackid)
distanceView = rootView.findViewById(R.id.statistics_data_distance)
stepsTitleView = rootView.findViewById(R.id.statistics_p_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 */
fun saveViewStateToTrack() {
if (track.latitude != 0.0 && track.longitude != 0.0) {
fun saveViewStateToTrack()
{
if (track.latitude != 0.0 && track.longitude != 0.0)
{
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
val steps: String
if (track.stepCount == -1f) {
if (track.stepCount == -1f)
{
steps = context.getString(R.string.statistics_sheet_p_steps_no_pedometer)
stepsTitleView.isGone = true
stepsView.isGone = true
}
else {
else
{
steps = track.stepCount.roundToInt().toString()
stepsTitleView.isVisible = true
stepsView.isVisible = true
@ -215,11 +221,12 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
// populate views
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
waypointsView.text = track.wayPoints.size.toString()
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)
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStop)
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits)

View File

@ -75,17 +75,41 @@
app:srcCompat="@drawable/ic_save_to_storage_24dp" />
<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_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/statistics_sheet_p_distance"
android:text="@string/statistics_sheet_p_trackid"
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_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
android:id="@+id/statistics_data_distance"
android:layout_width="wrap_content"

View File

@ -52,6 +52,7 @@
<string name="marker_description_time">Time</string>
<string name="marker_description_accuracy">Accuracy</string>
<!-- 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_steps">Steps taken:</string>
<string name="statistics_sheet_p_steps_no_pedometer">pedometer not available</string>