Was experimenting with automatic GPX export. Hardcoded path.

These changes are several months old, I am just now committing them
because I want to move on to a different experiment.
This commit is contained in:
voussoir 2023-03-04 21:29:18 -08:00
parent edcb149ac7
commit 7956f44ce4
15 changed files with 204 additions and 120 deletions

View file

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.y20k.trackbook"> package="org.y20k.trackbook">
<!-- USE GPS AND NETWORK - EXCLUDE NON-GPS DEVICES --> <!-- USE GPS AND NETWORK - EXCLUDE NON-GPS DEVICES -->
<uses-feature android:name="android.hardware.location.gps" android:required="true" /> <uses-feature android:name="android.hardware.location.gps" android:required="true" />
<uses-feature android:name="android.hardware.location.network" /> <uses-feature android:name="android.hardware.location.network" />
@ -17,13 +19,15 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" /> <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application <application
android:name=".Trackbook" android:name=".Trackbook"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<!-- MAIN ACTIVITY --> <!-- MAIN ACTIVITY -->

View file

@ -57,6 +57,7 @@ object Keys {
const val PREF_USE_IMPERIAL_UNITS: String = "prefUseImperialUnits" const val PREF_USE_IMPERIAL_UNITS: String = "prefUseImperialUnits"
const val PREF_GPS_ONLY: String = "prefGpsOnly" const val PREF_GPS_ONLY: String = "prefGpsOnly"
const val PREF_OMIT_RESTS: String = "prefOmitRests" const val PREF_OMIT_RESTS: String = "prefOmitRests"
const val PREF_AUTO_EXPORT_INTERVAL: String = "prefAutoExportInterval"
const val PREF_ALTITUDE_SMOOTHING_VALUE: String = "prefAltitudeSmoothingValue" const val PREF_ALTITUDE_SMOOTHING_VALUE: String = "prefAltitudeSmoothingValue"
const val PREF_LOCATION_ACCURACY_THRESHOLD: String = "prefLocationAccuracyThreshold" const val PREF_LOCATION_ACCURACY_THRESHOLD: String = "prefLocationAccuracyThreshold"
const val PREF_LOCATION_AGE_THRESHOLD: String = "prefLocationAgeThreshold" const val PREF_LOCATION_AGE_THRESHOLD: String = "prefLocationAgeThreshold"
@ -99,22 +100,25 @@ object Keys {
// default values // default values
val DEFAULT_DATE: Date = Date(0L) val DEFAULT_DATE: Date = Date(0L)
const val DEFAULT_RFC2822_DATE: String = "Thu, 01 Jan 1970 01:00:00 +0100" // --> Date(0) const val DEFAULT_RFC2822_DATE: String = "Thu, 01 Jan 1970 01:00:00 +0100" // --> Date(0)
const val ONE_HOUR_IN_MILLISECONDS: Int = 3600000 const val ONE_SECOND_IN_MILLISECONDS: Long = 1000
const val ONE_MINUTE_IN_MILLISECONDS: Long = 60 * ONE_SECOND_IN_MILLISECONDS
const val ONE_HOUR_IN_MILLISECONDS: Long = 60 * ONE_MINUTE_IN_MILLISECONDS
const val EMPTY_STRING_RESOURCE: Int = 0 const val EMPTY_STRING_RESOURCE: Int = 0
const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1000L // 1 second in milliseconds const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
const val ADD_WAYPOINT_TO_TRACK_INTERVAL: Long = 1000L // 1 second in milliseconds const val ADD_WAYPOINT_TO_TRACK_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
const val SAVE_TEMP_TRACK_INTERVAL: Long = 9000L // 9 seconds in milliseconds const val SAVE_TEMP_TRACK_INTERVAL: Long = 30 * ONE_SECOND_IN_MILLISECONDS
const val SIGNIFICANT_TIME_DIFFERENCE: Long = 120000L // 2 minutes in milliseconds const val SIGNIFICANT_TIME_DIFFERENCE: Long = 2 * ONE_MINUTE_IN_MILLISECONDS
const val STOP_OVER_THRESHOLD: Long = 300000L // 5 minutes in milliseconds const val STOP_OVER_THRESHOLD: Long = 5 * ONE_MINUTE_IN_MILLISECONDS
const val IMPLAUSIBLE_TRACK_START_SPEED: Double = 250.0 // 250 km/h const val IMPLAUSIBLE_TRACK_START_SPEED: Double = 250.0 // 250 km/h
const val DEFAULT_LATITUDE: Double = 71.172500 // latitude Nordkapp, Norway const val DEFAULT_LATITUDE: Double = 71.172500 // latitude Nordkapp, Norway
const val DEFAULT_LONGITUDE: Double = 25.784444 // longitude Nordkapp, Norway const val DEFAULT_LONGITUDE: Double = 25.784444 // longitude Nordkapp, Norway
const val DEFAULT_ACCURACY: Float = 300f // in meters const val DEFAULT_ACCURACY: Float = 300f // in meters
const val DEFAULT_ALTITUDE: Double = 0.0 const val DEFAULT_ALTITUDE: Double = 0.0
const val DEFAULT_TIME: Long = 0L const val DEFAULT_TIME: Long = 0L
const val DEFAULT_AUTO_EXPORT_INTERVAL: Int = 24
const val DEFAULT_ALTITUDE_SMOOTHING_VALUE: Int = 13 const val DEFAULT_ALTITUDE_SMOOTHING_VALUE: Int = 13
const val DEFAULT_THRESHOLD_LOCATION_ACCURACY: Int = 30 // 30 meters const val DEFAULT_THRESHOLD_LOCATION_ACCURACY: Int = 30 // 30 meters
const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 60000000000L // one minute in nanoseconds const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 60_000_000_000L // one minute in nanoseconds
const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f // 15 meters const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f // 15 meters
const val DEFAULT_ZOOM_LEVEL: Double = 16.0 const val DEFAULT_ZOOM_LEVEL: Double = 16.0
const val MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION: Int = 5 const val MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION: Int = 5

View file

@ -17,12 +17,16 @@
package org.y20k.trackbook package org.y20k.trackbook
import android.Manifest
import android.app.Activity
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.VmPolicy import android.os.StrictMode.VmPolicy
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
@ -31,6 +35,35 @@ import org.y20k.trackbook.helpers.AppThemeHelper
import org.y20k.trackbook.helpers.LogHelper import org.y20k.trackbook.helpers.LogHelper
import org.y20k.trackbook.helpers.PreferencesHelper import org.y20k.trackbook.helpers.PreferencesHelper
private const val REQUEST_EXTERNAL_STORAGE = 1
private val PERMISSIONS_STORAGE = arrayOf<String>(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
/**
* Checks if the app has permission to write to device storage
*
* If the app does not has permission then the user will be prompted to grant permissions
*
* @param activity
*/
fun verifyStoragePermissions(activity: Activity?)
{
// Check if we have write permission
val permission = ActivityCompat.checkSelfPermission(activity!!,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED)
{
// We don't have permission so prompt the user
ActivityCompat.requestPermissions(
activity,
PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE
)
}
}
/* /*
* MainActivity class * MainActivity class
*/ */
@ -48,7 +81,7 @@ class MainActivity : AppCompatActivity() {
/* Overrides onCreate from AppCompatActivity */ /* Overrides onCreate from AppCompatActivity */
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
verifyStoragePermissions(this)
// todo: remove after testing finished // todo: remove after testing finished
if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
StrictMode.setVmPolicy( StrictMode.setVmPolicy(

View file

@ -275,7 +275,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
private fun handleTrackingManagementMenu() { private fun handleTrackingManagementMenu() {
when (trackingState) { when (trackingState) {
Keys.STATE_TRACKING_PAUSED -> resumeTracking() Keys.STATE_TRACKING_PAUSED -> resumeTracking()
Keys.STATE_TRACKING_ACTIVE -> trackerService.stopTracking() Keys.STATE_TRACKING_ACTIVE -> trackerService.pauseTracking()
Keys.STATE_TRACKING_NOT_STARTED -> startTracking() Keys.STATE_TRACKING_NOT_STARTED -> startTracking()
} }
} }
@ -296,9 +296,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
else else
{ {
CoroutineScope(IO).launch { CoroutineScope(IO).launch {
track.save_json(activity as Context) trackerService.saveTrackAndClear(activity as Context)
track.save_gpx(activity as Context)
trackerService.clearTrack()
withContext(Main) { withContext(Main) {
// step 4: open track in TrackFragement // step 4: open track in TrackFragement
openTrack(track) openTrack(track)

View file

@ -123,6 +123,16 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceOmitRests.summaryOff = getString(R.string.pref_omit_rests_off) preferenceOmitRests.summaryOff = getString(R.string.pref_omit_rests_off)
preferenceOmitRests.setDefaultValue(DEFAULT_OMIT_RESTS) preferenceOmitRests.setDefaultValue(DEFAULT_OMIT_RESTS)
val preferenceAutoExportInterval: SeekBarPreference = SeekBarPreference(activity as Context)
preferenceAutoExportInterval.title = getString(R.string.pref_auto_export_interval_title)
preferenceAutoExportInterval.setIcon(R.drawable.ic_bar_chart_24)
preferenceAutoExportInterval.key = Keys.PREF_AUTO_EXPORT_INTERVAL
preferenceAutoExportInterval.summary = getString(R.string.pref_auto_export_interval_summary)
preferenceAutoExportInterval.showSeekBarValue = true
preferenceAutoExportInterval.min = 1
preferenceAutoExportInterval.max = 24
preferenceAutoExportInterval.setDefaultValue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
// set up "Altitude Smoothing" preference // set up "Altitude Smoothing" preference
// val preferenceAltitudeSmoothingValue: SeekBarPreference = SeekBarPreference(activity as Context) // val preferenceAltitudeSmoothingValue: SeekBarPreference = SeekBarPreference(activity as Context)
// preferenceAltitudeSmoothingValue.title = getString(R.string.pref_altitude_smoothing_value_title) // preferenceAltitudeSmoothingValue.title = getString(R.string.pref_altitude_smoothing_value_title)
@ -190,6 +200,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
screen.addPreference(preferenceCategoryAdvanced) screen.addPreference(preferenceCategoryAdvanced)
screen.addPreference(preferenceOmitRests) screen.addPreference(preferenceOmitRests)
// screen.addPreference(preferenceAltitudeSmoothingValue) // screen.addPreference(preferenceAltitudeSmoothingValue)
screen.addPreference(preferenceAutoExportInterval)
screen.addPreference(preferenceResetAdvanced) screen.addPreference(preferenceResetAdvanced)
screen.addPreference(preferenceCategoryAbout) screen.addPreference(preferenceCategoryAbout)
screen.addPreference(preferenceAppVersion) screen.addPreference(preferenceAppVersion)

View file

@ -49,7 +49,7 @@ import org.y20k.trackbook.helpers.LogHelper
import org.y20k.trackbook.helpers.MapOverlayHelper import org.y20k.trackbook.helpers.MapOverlayHelper
import org.y20k.trackbook.helpers.TrackHelper import org.y20k.trackbook.helpers.TrackHelper
import org.y20k.trackbook.ui.TrackFragmentLayoutHolder import org.y20k.trackbook.ui.TrackFragmentLayoutHolder
import java.io.File
class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener, MapOverlayHelper.MarkerListener { class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener, MapOverlayHelper.MarkerListener {
@ -138,6 +138,7 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
if (result.resultCode == Activity.RESULT_OK && result.data != null) if (result.resultCode == Activity.RESULT_OK && result.data != null)
{ {
val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri() val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri()
Toast.makeText(activity as Context, sourceUri.toString(), Toast.LENGTH_LONG).show()
val targetUri: Uri? = result.data?.data val targetUri: Uri? = result.data?.data
if (targetUri != null) if (targetUri != null)
{ {
@ -145,7 +146,8 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri) FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri)
} }
Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show() Toast.makeText(activity as Context, targetUri.toString(), Toast.LENGTH_LONG).show()
// Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show()
} }
} }
} }
@ -211,6 +213,16 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
LogHelper.e(TAG, "Unable to save GPX.") LogHelper.e(TAG, "Unable to save GPX.")
Toast.makeText(activity as Context, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show() Toast.makeText(activity as Context, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
} }
// val context = this.activity as Context
// val export_name: String = DateTimeHelper.convertToSortableDateString(layout.track.recordingStart) + Keys.GPX_FILE_EXTENSION
// val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri()
// // val targetUri: Uri = "file:///storage/emulated/0/Syncthing/GPX".toUri()
// val targetUri: Uri = File(File("/storage/emulated/0/Syncthing/GPX"), export_name).toUri()
// Toast.makeText(activity as Context, targetUri.toString(), Toast.LENGTH_LONG).show()
// CoroutineScope(Dispatchers.IO).launch {
// FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri)
// }
// Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show()
} }

View file

@ -58,12 +58,11 @@ class TrackerService: Service(), SensorEventListener
var useImperial: Boolean = false var useImperial: Boolean = false
var gpsOnly: Boolean = false var gpsOnly: Boolean = false
var omitRests: Boolean = true var omitRests: Boolean = true
var autoExportInterval: Int = Keys.DEFAULT_AUTO_EXPORT_INTERVAL
var currentBestLocation: Location = LocationHelper.getDefaultLocation() var currentBestLocation: Location = LocationHelper.getDefaultLocation()
var lastSave: Date = Keys.DEFAULT_DATE var lastTempSave: Date = Keys.DEFAULT_DATE
var lastAutoExport: Date = Keys.DEFAULT_DATE
var stepCountOffset: Float = 0f var stepCountOffset: Float = 0f
// The resumed flag will be true for the first point that is received after unpausing a
// recording, so that the distance travelled while paused is not added to the track.distance.
var resumed: Boolean = false
var track: Track = Track() var track: Track = Track()
var gpsLocationListenerRegistered: Boolean = false var gpsLocationListenerRegistered: Boolean = false
var networkLocationListenerRegistered: Boolean = false var networkLocationListenerRegistered: Boolean = false
@ -152,7 +151,7 @@ class TrackerService: Service(), SensorEventListener
fun clearTrack() fun clearTrack()
{ {
track = Track() track = Track()
resumed = false stepCountOffset = 0f
FileHelper.delete_temp_file(this as Context) FileHelper.delete_temp_file(this as Context)
trackingState = Keys.STATE_TRACKING_NOT_STARTED trackingState = Keys.STATE_TRACKING_NOT_STARTED
PreferencesHelper.saveTrackingState(trackingState) PreferencesHelper.saveTrackingState(trackingState)
@ -237,6 +236,7 @@ class TrackerService: Service(), SensorEventListener
gpsOnly = PreferencesHelper.loadGpsOnly() gpsOnly = PreferencesHelper.loadGpsOnly()
useImperial = PreferencesHelper.loadUseImperialUnits() useImperial = PreferencesHelper.loadUseImperialUnits()
omitRests = PreferencesHelper.loadOmitRests() omitRests = PreferencesHelper.loadOmitRests()
autoExportInterval = PreferencesHelper.loadAutoExportInterval()
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
@ -259,7 +259,7 @@ class TrackerService: Service(), SensorEventListener
LogHelper.i(TAG, "onDestroy called.") LogHelper.i(TAG, "onDestroy called.")
if (trackingState == Keys.STATE_TRACKING_ACTIVE) if (trackingState == Keys.STATE_TRACKING_ACTIVE)
{ {
stopTracking() pauseTracking()
} }
stopForeground(true) stopForeground(true)
notificationManager.cancel(Keys.TRACKER_SERVICE_NOTIFICATION_ID) // this call was not necessary prior to Android 12 notificationManager.cancel(Keys.TRACKER_SERVICE_NOTIFICATION_ID) // this call was not necessary prior to Android 12
@ -305,7 +305,7 @@ class TrackerService: Service(), SensorEventListener
} }
else if (intent.action == Keys.ACTION_STOP) else if (intent.action == Keys.ACTION_STOP)
{ {
stopTracking() pauseTracking()
} }
else if (intent.action == Keys.ACTION_START) else if (intent.action == Keys.ACTION_START)
{ {
@ -363,17 +363,31 @@ class TrackerService: Service(), SensorEventListener
{ {
// load temp track - returns an empty track if there is no temp file. // load temp track - returns an empty track if there is no temp file.
track = load_temp_track(this) track = load_temp_track(this)
// try to mark last waypoint as stopover if (track.wayPoints.isNotEmpty()) {
if (track.wayPoints.size > 0) { track.wayPoints.last().isStopOver = true
val lastWayPointIndex = track.wayPoints.size - 1
track.wayPoints[lastWayPointIndex].isStopOver = true
} }
resumed = true track.resumed = true
// calculate length of recording break track.recordingPaused += (GregorianCalendar.getInstance().time.time - track.recordingStop.time)
track.recordingPaused += TrackHelper.calculateDurationOfPause(track.recordingStop)
startTracking(newTrack = false) startTracking(newTrack = false)
} }
fun saveTrackAndClear(context: Context)
{
this.pauseTracking()
track.save_all_files(context)
this.clearTrack()
}
fun saveTrackAndStartNew(context: Context)
{
if (track.wayPoints.isNotEmpty())
{
track.save_all_files(context)
}
track = Track()
FileHelper.delete_temp_file(this as Context)
}
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)
@ -390,7 +404,6 @@ class TrackerService: Service(), SensorEventListener
// set up new track // set up new track
if (newTrack) { if (newTrack) {
track = Track() track = Track()
resumed = false
stepCountOffset = 0f stepCountOffset = 0f
} }
trackingState = Keys.STATE_TRACKING_ACTIVE trackingState = Keys.STATE_TRACKING_ACTIVE
@ -400,11 +413,10 @@ class TrackerService: Service(), SensorEventListener
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification()) startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
} }
fun stopTracking() fun pauseTracking()
{ {
track.recordingStop = GregorianCalendar.getInstance().time track.recordingStop = GregorianCalendar.getInstance().time
val context: Context = this CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) }
CoroutineScope(IO).launch { track.save_temp_suspended(context) }
trackingState = Keys.STATE_TRACKING_PAUSED trackingState = Keys.STATE_TRACKING_PAUSED
PreferencesHelper.saveTrackingState(trackingState) PreferencesHelper.saveTrackingState(trackingState)
@ -439,6 +451,9 @@ class TrackerService: Service(), SensorEventListener
Keys.PREF_OMIT_RESTS -> { Keys.PREF_OMIT_RESTS -> {
omitRests = PreferencesHelper.loadOmitRests() omitRests = PreferencesHelper.loadOmitRests()
} }
Keys.PREF_AUTO_EXPORT_INTERVAL -> {
autoExportInterval = PreferencesHelper.loadAutoExportInterval()
}
} }
} }
/* /*
@ -462,9 +477,10 @@ class TrackerService: Service(), SensorEventListener
{ {
override fun run() { override fun run() {
// add waypoint to track - step count is continuously updated in onSensorChanged // add waypoint to track - step count is continuously updated in onSensorChanged
val success = track.add_waypoint(currentBestLocation, omitRests, resumed) val success = track.add_waypoint(currentBestLocation, omitRests, track.resumed)
val now: Date = GregorianCalendar.getInstance().time
if (success) { if (success) {
resumed = false track.resumed = false
// store previous smoothed altitude // store previous smoothed altitude
val previousAltitude: Double = altitudeValues.getAverage() val previousAltitude: Double = altitudeValues.getAverage()
@ -488,11 +504,14 @@ class TrackerService: Service(), SensorEventListener
} }
// save a temp track // save a temp track
val now: Date = GregorianCalendar.getInstance().time if (now.time - lastTempSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) {
if (now.time - lastSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) { lastTempSave = now
lastSave = now
CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) } CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) }
} }
}
if (now.time - track.recordingStart.time > (autoExportInterval * Keys.ONE_HOUR_IN_MILLISECONDS)) {
saveTrackAndStartNew(this@TrackerService)
} }
// update notification // update notification
displayNotification() displayNotification()

View file

@ -16,23 +16,36 @@
package org.y20k.trackbook.core package org.y20k.trackbook.core
import android.Manifest
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.os.Parcelable import android.os.Parcelable
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.core.app.ActivityCompat
import androidx.core.net.toUri
import com.google.gson.annotations.Expose import com.google.gson.annotations.Expose
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import org.y20k.trackbook.Keys
import org.y20k.trackbook.R
import org.y20k.trackbook.helpers.DateTimeHelper
import org.y20k.trackbook.helpers.FileHelper
import org.y20k.trackbook.helpers.LocationHelper
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random import kotlin.random.Random
import kotlinx.parcelize.Parcelize
import org.y20k.trackbook.Keys
import org.y20k.trackbook.helpers.DateTimeHelper
import org.y20k.trackbook.helpers.FileHelper
import org.y20k.trackbook.helpers.LocationHelper
/* /*
* Track data class * Track data class
@ -50,6 +63,9 @@ data class Track (
@Expose var recordingStart: Date = GregorianCalendar.getInstance().time, @Expose var recordingStart: Date = GregorianCalendar.getInstance().time,
@Expose var dateString: String = DateTimeHelper.convertToReadableDate(recordingStart), @Expose var dateString: String = DateTimeHelper.convertToReadableDate(recordingStart),
@Expose var recordingStop: Date = recordingStart, @Expose var recordingStop: Date = recordingStart,
// The resumed flag will be true for the first point that is received after unpausing a
// recording, so that the distance travelled while paused is not added to the track.distance.
@Expose var resumed: Boolean = false,
@Expose var maxAltitude: Double = 0.0, @Expose var maxAltitude: Double = 0.0,
@Expose var minAltitude: Double = 0.0, @Expose var minAltitude: Double = 0.0,
@Expose var positiveElevation: Double = 0.0, @Expose var positiveElevation: Double = 0.0,
@ -165,22 +181,29 @@ data class Track (
return File(context.getExternalFilesDir(Keys.FOLDER_GPX), basename) return File(context.getExternalFilesDir(Keys.FOLDER_GPX), basename)
} }
fun get_export_gpx_file(context: Context): File
{
val basename: String = DateTimeHelper.convertToSortableDateString(this.recordingStart) + Keys.GPX_FILE_EXTENSION
return File(File("/storage/emulated/0/Syncthing/GPX"), basename)
}
fun get_json_file(context: Context): File fun get_json_file(context: Context): File
{ {
val basename: String = this.id.toString() + Keys.TRACKBOOK_FILE_EXTENSION val basename: String = this.id.toString() + Keys.TRACKBOOK_FILE_EXTENSION
return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), basename) return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), basename)
} }
fun save_both(context: Context) fun save_all_files(context: Context)
{ {
this.save_json(context) this.save_json(context)
this.save_gpx(context) this.save_gpx(context)
this.save_export_gpx(context)
} }
suspend fun save_both_suspended(context: Context) suspend fun save_all_files_suspended(context: Context)
{ {
return suspendCoroutine { cont -> return suspendCoroutine { cont ->
cont.resume(this.save_both(context)) cont.resume(this.save_all_files(context))
} }
} }
@ -198,6 +221,23 @@ data class Track (
} }
} }
fun save_export_gpx(context: Context)
{
val gpx: String = this.to_gpx()
val outputfile: File = this.get_export_gpx_file(context)
FileHelper.write_text_file_noblank(gpx, outputfile)
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, outputfile.toString(), Toast.LENGTH_SHORT).show()
}
}
suspend fun save_export_gpx_suspended(context: Context)
{
return suspendCoroutine { cont ->
cont.resume(this.save_export_gpx(context))
}
}
fun save_json(context: Context) fun save_json(context: Context)
{ {
val json: String = this.to_json() val json: String = this.to_json()

View file

@ -55,7 +55,8 @@ object FileHelper {
suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) { suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) {
return suspendCoroutine { cont -> return suspendCoroutine { cont ->
track.name = newName track.name = newName
track.save_both(context) track.save_json(context)
track.save_gpx(context)
cont.resume(Unit) cont.resume(Unit)
} }
} }
@ -71,7 +72,7 @@ object FileHelper {
/* Copies file to specified target */ /* Copies file to specified target */
private fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) { fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
val inputStream = context.contentResolver.openInputStream(originalFileUri) val inputStream = context.contentResolver.openInputStream(originalFileUri)
val outputStream = context.contentResolver.openOutputStream(targetFileUri) val outputStream = context.contentResolver.openOutputStream(targetFileUri)
if (outputStream != null) { if (outputStream != null) {

View file

@ -71,9 +71,11 @@ object PreferencesHelper {
return sharedPreferences.getBoolean(Keys.PREF_OMIT_RESTS, true) return sharedPreferences.getBoolean(Keys.PREF_OMIT_RESTS, true)
} }
// /* Load altitude smoothing value */ fun loadAutoExportInterval(): Int {
return sharedPreferences.getInt(Keys.PREF_AUTO_EXPORT_INTERVAL, Keys.DEFAULT_AUTO_EXPORT_INTERVAL)
}
// fun loadAltitudeSmoothingValue(): Int { // fun loadAltitudeSmoothingValue(): Int {
// // load current setting
// return sharedPreferences.getInt(Keys.PREF_ALTITUDE_SMOOTHING_VALUE, Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE) // return sharedPreferences.getInt(Keys.PREF_ALTITUDE_SMOOTHING_VALUE, Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
// } // }

View file

@ -32,10 +32,6 @@ object TrackHelper {
/* Adds given locatiom as waypoint to track */ /* Adds given locatiom as waypoint to track */
/* Calculates time passed since last stop of recording */
fun calculateDurationOfPause(recordingStop: Date): Long = GregorianCalendar.getInstance().time.time - recordingStop.time
/* Toggles starred flag for given position */ /* Toggles starred flag for given position */
fun toggle_waypoint_starred(context: Context, track: Track, latitude: Double, longitude: Double) fun toggle_waypoint_starred(context: Context, track: Track, latitude: Double, longitude: Double)
{ {

View file

@ -104,7 +104,6 @@ object UiHelper {
override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
// disable swipe for statistics element // disable swipe for statistics element
if (viewHolder is TracklistAdapter.ElementStatisticsViewHolder) return 0
return super.getSwipeDirs(recyclerView, viewHolder) return super.getSwipeDirs(recyclerView, viewHolder)
} }

View file

@ -75,51 +75,28 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/* Overrides onCreateViewHolder from RecyclerView.Adapter */ /* Overrides onCreateViewHolder from RecyclerView.Adapter */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
{ {
when (viewType) {
Keys.VIEW_TYPE_STATISTICS -> {
val v = LayoutInflater.from(parent.context).inflate(R.layout.element_statistics, parent, false)
return ElementStatisticsViewHolder(v)
}
else -> {
val v = LayoutInflater.from(parent.context).inflate(R.layout.element_track, parent, false) val v = LayoutInflater.from(parent.context).inflate(R.layout.element_track, parent, false)
return ElementTrackViewHolder(v) return ElementTrackViewHolder(v)
} }
}
}
/* Overrides getItemViewType */ /* Overrides getItemViewType */
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
if (position == 0) {
return Keys.VIEW_TYPE_STATISTICS
} else {
return Keys.VIEW_TYPE_TRACK return Keys.VIEW_TYPE_TRACK
} }
}
/* Overrides getItemCount from RecyclerView.Adapter */ /* Overrides getItemCount from RecyclerView.Adapter */
override fun getItemCount(): Int { override fun getItemCount(): Int {
// +1 because of the total statistics element return tracklist.tracks.size
return tracklist.tracks.size + 1
} }
/* Overrides onBindViewHolder from RecyclerView.Adapter */ /* Overrides onBindViewHolder from RecyclerView.Adapter */
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
{ {
when (holder) val positionInTracklist: Int = position
{ val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder
// CASE STATISTICS ELEMENT
is ElementStatisticsViewHolder -> {
val elementStatisticsViewHolder: ElementStatisticsViewHolder = holder
elementStatisticsViewHolder.totalDistanceView.text = LengthUnitHelper.convertDistanceToString(tracklist.get_total_distance(), useImperial)
}
// CASE TRACK ELEMENT
is ElementTrackViewHolder -> {
val positionInTracklist: Int = position - 1 // Element 0 is the statistics element.
val elementTrackViewHolder: ElementTrackViewHolder = holder
elementTrackViewHolder.trackNameView.text = tracklist.tracks[positionInTracklist].name elementTrackViewHolder.trackNameView.text = tracklist.tracks[positionInTracklist].name
elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist) elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist)
when (tracklist.tracks[positionInTracklist].starred) { when (tracklist.tracks[positionInTracklist].starred) {
@ -133,25 +110,21 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
toggleStarred(it, positionInTracklist) toggleStarred(it, positionInTracklist)
} }
} }
}
}
/* Get track name for given position */ /* Get track name for given position */
fun getTrackName(positionInRecyclerView: Int): String fun getTrackName(positionInRecyclerView: Int): String
{ {
// Minus 1 because first position is always the statistics element return tracklist.tracks[positionInRecyclerView].name
return tracklist.tracks[positionInRecyclerView - 1].name
} }
fun delete_track_at_position(context: Context, ui_index: Int) fun delete_track_at_position(context: Context, index: Int)
{ {
val track_index = ui_index - 1 // position 0 is the statistics element val track = tracklist.tracks[index]
val track = tracklist.tracks[track_index]
track.delete(context) track.delete(context)
tracklist.tracks.remove(track) tracklist.tracks.remove(track)
notifyItemChanged(0) notifyItemRemoved(index)
notifyItemRemoved(ui_index) notifyItemRangeChanged(index, this.itemCount);
} }
suspend fun delete_track_at_position_suspended(context: Context, position: Int) suspend fun delete_track_at_position_suspended(context: Context, position: Int)
@ -167,10 +140,12 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
if (index == -1) { if (index == -1) {
return return
} }
delete_track_at_position(context, index + 1) tracklist.tracks[index].delete(context)
tracklist.tracks.removeAt(index)
notifyItemRemoved(index)
notifyItemRangeChanged(index, this.itemCount);
} }
/* Returns if the adapter is empty */
fun isEmpty(): Boolean { fun isEmpty(): Boolean {
return tracklist.tracks.size == 0 return tracklist.tracks.size == 0
} }
@ -249,16 +224,4 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/* /*
* End of inner class * End of inner class
*/ */
/*
* Inner class: ViewHolder for a statistics element
*/
inner class ElementStatisticsViewHolder (elementStatisticsLayout: View): RecyclerView.ViewHolder(elementStatisticsLayout) {
val totalDistanceView: TextView = elementStatisticsLayout.findViewById(R.id.total_distance_data)
}
/*
* End of inner class
*/
} }

View file

@ -187,7 +187,7 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
mapView.overlays.add(trackSpecialMarkersOverlay) mapView.overlays.add(trackSpecialMarkersOverlay)
} }
// save track // save track
CoroutineScope(Dispatchers.IO).launch { track.save_both_suspended(context) } CoroutineScope(Dispatchers.IO).launch { track.save_all_files_suspended(context) }
} }

View file

@ -85,6 +85,8 @@
<string name="pref_accuracy_threshold_title">Accuracy Threshold</string> <string name="pref_accuracy_threshold_title">Accuracy Threshold</string>
<string name="pref_altitude_smoothing_value_summary" translatable="false">Number of waypoints used to smooth the elevation curve.</string> <string name="pref_altitude_smoothing_value_summary" translatable="false">Number of waypoints used to smooth the elevation curve.</string>
<string name="pref_altitude_smoothing_value_title" translatable="false">Altitude Smoothing</string> <string name="pref_altitude_smoothing_value_title" translatable="false">Altitude Smoothing</string>
<string name="pref_auto_export_interval_summary">Automatically export GPX file after this many hours.</string>
<string name="pref_auto_export_interval_title">Auto Export Interval</string>
<string name="pref_advanced_title">Advanced</string> <string name="pref_advanced_title">Advanced</string>
<string name="pref_delete_non_starred_summary">Delete all recordings in \"Tracks\" that are not starred.</string> <string name="pref_delete_non_starred_summary">Delete all recordings in \"Tracks\" that are not starred.</string>
<string name="pref_delete_non_starred_title">Delete Non-Starred Recordings</string> <string name="pref_delete_non_starred_title">Delete Non-Starred Recordings</string>
@ -98,7 +100,7 @@
<string name="pref_imperial_measurement_units_title">Use Imperial Measurements</string> <string name="pref_imperial_measurement_units_title">Use Imperial Measurements</string>
<string name="pref_omit_rests_on">Waypoints will not be recorded if they are too close to the previous waypoint.</string> <string name="pref_omit_rests_on">Waypoints will not be recorded if they are too close to the previous waypoint.</string>
<string name="pref_omit_rests_off">All waypoints will be recorded, even while standing still.</string> <string name="pref_omit_rests_off">All waypoints will be recorded, even while standing still.</string>
<string name="pref_omit_rests_title">Omit points during rests</string> <string name="pref_omit_rests_title">Omit repeated points</string>
<string name="pref_report_issue_summary">Report bugs and suggest improvements on GitHub.</string> <string name="pref_report_issue_summary">Report bugs and suggest improvements on GitHub.</string>
<string name="pref_report_issue_title">Report Issue</string> <string name="pref_report_issue_title">Report Issue</string>
<string name="pref_reset_advanced_summary">Reset advanced settings to defaults.</string> <string name="pref_reset_advanced_summary">Reset advanced settings to defaults.</string>