checkpoint

This commit is contained in:
voussoir 2023-03-06 20:52:41 -08:00
parent 47531768d1
commit 2568af3bb1
49 changed files with 825 additions and 1512 deletions

View file

@ -4,12 +4,12 @@ apply plugin: 'kotlin-parcelize'
apply plugin: 'androidx.navigation.safeargs.kotlin' apply plugin: 'androidx.navigation.safeargs.kotlin'
android { android {
compileSdkVersion 31 compileSdkVersion 33
defaultConfig { defaultConfig {
applicationId 'net.voussoir.trackbook' applicationId 'net.voussoir.trkpt'
minSdkVersion 25 minSdkVersion 25
targetSdkVersion 31 targetSdk 32
versionCode 50 versionCode 50
versionName '2.1.2' versionName '2.1.2'
resConfigs "en", "da", "de", "fr", "hr", "id", "it", "ja", "nb-rNO", "nl", "pl", "pt-rBR", "ru", "sv", "tr", "zh-rCN" resConfigs "en", "da", "de", "fr", "hr", "id", "it", "ja", "nb-rNO", "nl", "pl", "pt-rBR", "ru", "sv", "tr", "zh-rCN"
@ -51,25 +51,25 @@ android {
dependencies { dependencies {
// Kotlin // Kotlin
def coroutinesVersion = "1.5.2" def coroutinesVersion = '1.6.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
// AndroidX // AndroidX
def navigationVersion = "2.3.5" def navigationVersion = '2.5.3'
implementation "androidx.activity:activity-ktx:1.4.0" implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.core:core-ktx:1.9.0'
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" implementation "androidx.navigation:navigation-ui-ktx:2.5.3"
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'com.google.android.material:material:1.6.0-alpha03' implementation 'com.google.android.material:material:1.9.0-alpha02'
// Gson // Gson
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.10.1'
// OpenStreetMap // OpenStreetMap
implementation 'org.osmdroid:osmdroid-android:6.1.11' implementation 'org.osmdroid:osmdroid-android:6.1.14'
} }

View file

@ -22,7 +22,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application <application
android:name=".Trackbook" android:name="org.y20k.trackbook.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"

View file

@ -26,7 +26,7 @@ import java.util.*
object Keys { object Keys {
// application name // application name
const val APPLICATION_NAME: String = "Trackbook" const val APPLICATION_NAME: String = "trkpt"
// version numbers // version numbers
const val CURRENT_TRACK_FORMAT_VERSION: Int = 4 const val CURRENT_TRACK_FORMAT_VERSION: Int = 4
@ -39,9 +39,10 @@ object Keys {
// args // args
const val ARG_TRACK_TITLE: String = "ArgTrackTitle" const val ARG_TRACK_TITLE: String = "ArgTrackTitle"
const val ARG_TRACK_FILE_URI: String = "ArgTrackFileUri" const val ARG_TRACK_ID: String = "ArgTrackID"
const val ARG_GPX_FILE_URI: String = "ArgGpxFileUri" const val ARG_TRACK_DEVICE_ID: String = "ArgTrackDeviceID"
const val ARG_TRACK_ID: String = "ArgTrackId" const val ARG_TRACK_START_TIME: String = "ArgTrackStartTime"
const val ARG_TRACK_STOP_TIME: String = "ArgTrackStopTime"
// preferences // preferences
const val PREF_ONE_TIME_HOUSEKEEPING_NECESSARY = "ONE_TIME_HOUSEKEEPING_NECESSARY_VERSIONCODE_38" // increment to current app version code to trigger housekeeping that runs only once const val PREF_ONE_TIME_HOUSEKEEPING_NECESSARY = "ONE_TIME_HOUSEKEEPING_NECESSARY_VERSIONCODE_38" // increment to current app version code to trigger housekeeping that runs only once
@ -57,15 +58,12 @@ 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_COMMIT_INTERVAL: String = "prefCommitInterval"
const val PREF_ALTITUDE_SMOOTHING_VALUE: String = "prefAltitudeSmoothingValue" const val PREF_DEVICE_ID: String = "prefDeviceID"
const val PREF_LOCATION_ACCURACY_THRESHOLD: String = "prefLocationAccuracyThreshold"
const val PREF_LOCATION_AGE_THRESHOLD: String = "prefLocationAgeThreshold"
// states // states
const val STATE_TRACKING_NOT_STARTED: Int = 0 const val STATE_TRACKING_STOPPED: Int = 0
const val STATE_TRACKING_ACTIVE: Int = 1 const val STATE_TRACKING_ACTIVE: Int = 1
const val STATE_TRACKING_PAUSED: Int = 2
const val STATE_THEME_FOLLOW_SYSTEM: String = "stateFollowSystem" const val STATE_THEME_FOLLOW_SYSTEM: String = "stateFollowSystem"
const val STATE_THEME_LIGHT_MODE: String = "stateLightMode" const val STATE_THEME_LIGHT_MODE: String = "stateLightMode"
const val STATE_THEME_DARK_MODE: String = "stateDarkMode" const val STATE_THEME_DARK_MODE: String = "stateDarkMode"
@ -105,9 +103,9 @@ object Keys {
const val ONE_HOUR_IN_MILLISECONDS: Long = 60 * ONE_MINUTE_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 = 1 * ONE_SECOND_IN_MILLISECONDS const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
const val ADD_WAYPOINT_TO_TRACK_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS const val TRACKING_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
const val SAVE_TEMP_TRACK_INTERVAL: Long = 30 * ONE_SECOND_IN_MILLISECONDS const val SAVE_TEMP_TRACK_INTERVAL: Long = 30 * ONE_SECOND_IN_MILLISECONDS
const val SIGNIFICANT_TIME_DIFFERENCE: Long = 2 * ONE_MINUTE_IN_MILLISECONDS const val SIGNIFICANT_TIME_DIFFERENCE: Long = 1 * ONE_MINUTE_IN_MILLISECONDS
const val STOP_OVER_THRESHOLD: Long = 5 * ONE_MINUTE_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
@ -115,14 +113,12 @@ object Keys {
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 COMMIT_INTERVAL: Int = 30
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 = 60_000_000_000L // 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 MAX_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION: Int = 20
const val ALTITUDE_MEASUREMENT_ERROR_THRESHOLD = 10 // altitude changes of 10 meter or more (per 15 seconds) are being discarded const val ALTITUDE_MEASUREMENT_ERROR_THRESHOLD = 10 // altitude changes of 10 meter or more (per 15 seconds) are being discarded
// notification // notification

View file

@ -17,7 +17,6 @@
package org.y20k.trackbook package org.y20k.trackbook
import YesNoDialog
import android.Manifest import android.Manifest
import android.content.* import android.content.*
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -28,14 +27,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
import org.y20k.trackbook.helpers.* import org.y20k.trackbook.helpers.*
import org.y20k.trackbook.ui.MapFragmentLayoutHolder import org.y20k.trackbook.ui.MapFragmentLayoutHolder
@ -43,34 +35,34 @@ import org.y20k.trackbook.ui.MapFragmentLayoutHolder
/* /*
* MapFragment class * MapFragment class
*/ */
class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelper.MarkerListener { class MapFragment : Fragment(), MapOverlayHelper.MarkerListener
{
/* Define log tag */ /* Define log tag */
private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java) private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java)
/* Main class variables */ /* Main class variables */
private var bound: Boolean = false private var bound: Boolean = false
private val handler: Handler = Handler(Looper.getMainLooper()) private val handler: Handler = Handler(Looper.getMainLooper())
private var trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED private var trackingState: Int = Keys.STATE_TRACKING_STOPPED
private var gpsProviderActive: Boolean = false private var gpsProviderActive: Boolean = false
private var networkProviderActive: Boolean = false private var networkProviderActive: Boolean = false
private var track: Track = Track() private lateinit var track: Track
private lateinit var currentBestLocation: Location private lateinit var currentBestLocation: Location
private lateinit var layout: MapFragmentLayoutHolder private lateinit var layout: MapFragmentLayoutHolder
private lateinit var trackerService: TrackerService private lateinit var trackerService: TrackerService
/* Overrides onCreate from Fragment */ /* Overrides onCreate from Fragment */
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// TODO make only MapFragment's status bar transparent - see: https://gist.github.com/Dvik/a3de88d39da9d1d6d175025a56c5e797#file-viewextension-kt and https://proandroiddev.com/android-full-screen-ui-with-transparent-status-bar-ef52f3adde63 // TODO make only MapFragment's status bar transparent - see:
// https://gist.github.com/Dvik/a3de88d39da9d1d6d175025a56c5e797#file-viewextension-kt and
// https://proandroiddev.com/android-full-screen-ui-with-transparent-status-bar-ef52f3adde63
// get current best location // get current best location
currentBestLocation = LocationHelper.getLastKnownLocation(activity as Context) currentBestLocation = LocationHelper.getLastKnownLocation(activity as Context)
// get saved tracking state // get saved tracking state
trackingState = PreferencesHelper.loadTrackingState() trackingState = PreferencesHelper.loadTrackingState()
} }
/* Overrides onStop from Fragment */ /* Overrides onStop from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// initialize layout // initialize layout
@ -84,30 +76,10 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
layout.mainButton.setOnClickListener { layout.mainButton.setOnClickListener {
handleTrackingManagementMenu() handleTrackingManagementMenu()
} }
layout.saveButton.setOnClickListener {
saveTrack()
}
layout.clearButton.setOnClickListener {
if (track.wayPoints.isEmpty())
{
// This might seem useless since it's already empty, but there are other UI updates
// that occur as part of cleartrack so we may as well take advantage of those.
trackerService.clearTrack()
}
else {
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(
context=activity as Context,
type = Keys.DIALOG_DELETE_CURRENT_RECORDING,
message = R.string.dialog_delete_current_recording_message,
yesButton = R.string.dialog_delete_current_recording_button_discard
)
}
}
return layout.rootView return layout.rootView
} }
/* Overrides onStart from Fragment */ /* Overrides onStart from Fragment */
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -119,7 +91,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
activity?.bindService(Intent(activity, TrackerService::class.java), connection, Context.BIND_AUTO_CREATE) activity?.bindService(Intent(activity, TrackerService::class.java), connection, Context.BIND_AUTO_CREATE)
} }
/* Overrides onResume from Fragment */ /* Overrides onResume from Fragment */
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -129,7 +100,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
// } // }
} }
/* Overrides onPause from Fragment */ /* Overrides onPause from Fragment */
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
@ -137,17 +107,20 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) { if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) {
trackerService.removeGpsLocationListener() trackerService.removeGpsLocationListener()
trackerService.removeNetworkLocationListener() trackerService.removeNetworkLocationListener()
trackerService.trackbook.database.commit()
} }
} }
/* Overrides onStop from Fragment */ /* Overrides onStop from Fragment */
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
// unbind from TrackerService // unbind from TrackerService
if (bound)
{
activity?.unbindService(connection) activity?.unbindService(connection)
handleServiceUnbind() handleServiceUnbind()
} }
}
/* Register the permission launcher for requesting location */ /* Register the permission launcher for requesting location */
private val requestLocationPermissionLauncher = registerForActivityResult(RequestPermission()) { isGranted: Boolean -> private val requestLocationPermissionLauncher = registerForActivityResult(RequestPermission()) { isGranted: Boolean ->
@ -171,14 +144,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
trackerService.startTracking() trackerService.startTracking()
} }
/* Register the permission launcher for resuming the tracking service */
private val resumeTrackingPermissionLauncher = registerForActivityResult(RequestPermission()) { isGranted: Boolean ->
logPermissionRequestResult(isGranted)
// start service via intent so that it keeps running after unbind
startTrackerService()
trackerService.resumeTracking()
}
/* Logs the request result of the Activity Recognition permission launcher */ /* Logs the request result of the Activity Recognition permission launcher */
private fun logPermissionRequestResult(isGranted: Boolean) { private fun logPermissionRequestResult(isGranted: Boolean) {
if (isGranted) { if (isGranted) {
@ -188,68 +153,24 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
} }
} }
/* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
super.onYesNoDialog(type, dialogResult, payload, payloadString)
when (type) {
Keys.DIALOG_RESUME_EMPTY_RECORDING -> {
when (dialogResult) {
// user tapped resume
true -> {
trackerService.resumeTracking()
}
}
}
Keys.DIALOG_DELETE_CURRENT_RECORDING -> {
when (dialogResult) {
true -> {
trackerService.clearTrack()
}
}
}
}
}
/* Overrides onMarkerTapped from MarkerListener */
override fun onMarkerTapped(latitude: Double, longitude: Double) {
super.onMarkerTapped(latitude, longitude)
if (bound) {
TrackHelper.toggle_waypoint_starred(activity as Context, track, latitude, longitude)
layout.overlayCurrentTrack(track, trackingState)
trackerService.track = track
}
}
/* Start recording waypoints */ /* Start recording waypoints */
private fun startTracking() { private fun startTracking() {
// request activity recognition permission on Android Q+ if denied // request activity recognition permission on Android Q+ if denied
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_DENIED) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_DENIED)
{
startTrackingPermissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION) startTrackingPermissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION)
} else { }
else
{
// start service via intent so that it keeps running after unbind // start service via intent so that it keeps running after unbind
startTrackerService() startTrackerService()
trackerService.startTracking() trackerService.startTracking()
} }
} }
/* Resume recording waypoints */
private fun resumeTracking() {
// request activity recognition permission on Android Q+ if denied
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_DENIED) {
resumeTrackingPermissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION)
} else {
// start service via intent so that it keeps running after unbind
startTrackerService()
trackerService.resumeTracking()
}
}
/* Start tracker service */ /* Start tracker service */
private fun startTrackerService() { private fun startTrackerService()
{
val intent = Intent(activity, TrackerService::class.java) val intent = Intent(activity, TrackerService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// ... start service in foreground to prevent it being killed on Oreo // ... start service in foreground to prevent it being killed on Oreo
@ -259,9 +180,9 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
} }
} }
/* Handles state when service is being unbound */ /* Handles state when service is being unbound */
private fun handleServiceUnbind() { private fun handleServiceUnbind()
{
bound = false bound = false
// unregister listener for changes in shared preferences // unregister listener for changes in shared preferences
PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener) PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener)
@ -269,61 +190,25 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
handler.removeCallbacks(periodicLocationRequestRunnable) handler.removeCallbacks(periodicLocationRequestRunnable)
} }
/* Starts / pauses tracking and toggles the recording sub menu_bottom_navigation */ /* Starts / pauses tracking and toggles the recording sub menu_bottom_navigation */
private fun handleTrackingManagementMenu() { private fun handleTrackingManagementMenu()
{
when (trackingState) { when (trackingState) {
Keys.STATE_TRACKING_PAUSED -> resumeTracking()
Keys.STATE_TRACKING_ACTIVE -> trackerService.pauseTracking() Keys.STATE_TRACKING_ACTIVE -> trackerService.pauseTracking()
Keys.STATE_TRACKING_NOT_STARTED -> startTracking() Keys.STATE_TRACKING_STOPPED -> startTracking()
} }
} }
/* Saves track - shows dialog, if recording is still empty */
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
)
}
else
{
CoroutineScope(IO).launch {
trackerService.saveTrackAndClear(activity as Context)
withContext(Main) {
// step 4: open track in TrackFragement
openTrack(track)
}
}
}
}
/* Opens a track in TrackFragment */
private fun openTrack(track: Track) {
val bundle: Bundle = Bundle()
bundle.putString(Keys.ARG_TRACK_TITLE, track.name)
bundle.putString(Keys.ARG_TRACK_FILE_URI, track.get_json_file(activity as Context).toUri().toString())
bundle.putString(Keys.ARG_GPX_FILE_URI, track.get_gpx_file(activity as Context).toUri().toString())
bundle.putLong(Keys.ARG_TRACK_ID, track.id)
findNavController().navigate(R.id.fragment_track, bundle)
}
/* /*
* Defines the listener for changes in shared preferences * Defines the listener for changes in shared preferences
*/ */
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) { when (key)
Keys.PREF_TRACKING_STATE -> { {
if (activity != null) { Keys.PREF_TRACKING_STATE ->
{
if (activity != null)
{
trackingState = PreferencesHelper.loadTrackingState() trackingState = PreferencesHelper.loadTrackingState()
layout.updateMainButton(trackingState) layout.updateMainButton(trackingState)
} }
@ -334,7 +219,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
* End of declaration * End of declaration
*/ */
/* /*
* Defines callbacks for service binding, passed to bindService() * Defines callbacks for service binding, passed to bindService()
*/ */
@ -362,7 +246,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
* End of declaration * End of declaration
*/ */
/* /*
* Runnable: Periodically requests location * Runnable: Periodically requests location
*/ */
@ -377,9 +260,11 @@ 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(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
layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive) layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
// use the handler to start runnable again after specified delay // use the handler to start runnable again after specified delay
@ -389,5 +274,4 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
/* /*
* End of declaration * End of declaration
*/ */
} }

View file

@ -26,6 +26,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import androidx.preference.* import androidx.preference.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -33,13 +34,13 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.y20k.trackbook.core.Tracklist
import org.y20k.trackbook.core.load_tracklist
import org.y20k.trackbook.helpers.AppThemeHelper import org.y20k.trackbook.helpers.AppThemeHelper
import org.y20k.trackbook.helpers.FileHelper import org.y20k.trackbook.helpers.FileHelper
import org.y20k.trackbook.helpers.LengthUnitHelper import org.y20k.trackbook.helpers.LengthUnitHelper
import org.y20k.trackbook.helpers.LogHelper import org.y20k.trackbook.helpers.LogHelper
import org.y20k.trackbook.helpers.PreferencesHelper
import org.y20k.trackbook.helpers.random_int
import kotlin.random.Random
/* /*
* SettingsFragment class * SettingsFragment class
@ -64,6 +65,10 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
val context = preferenceManager.context val context = preferenceManager.context
val screen = preferenceManager.createPreferenceScreen(context) val screen = preferenceManager.createPreferenceScreen(context)
val preferenceCategoryGeneral: PreferenceCategory = PreferenceCategory(activity as Context)
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
screen.addPreference(preferenceCategoryGeneral)
// set up "Restrict to GPS" preference // set up "Restrict to GPS" preference
val preferenceGpsOnly: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context) val preferenceGpsOnly: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
preferenceGpsOnly.isSingleLineTitle = false preferenceGpsOnly.isSingleLineTitle = false
@ -73,6 +78,8 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceGpsOnly.summaryOn = getString(R.string.pref_gps_only_summary_gps_only) preferenceGpsOnly.summaryOn = getString(R.string.pref_gps_only_summary_gps_only)
preferenceGpsOnly.summaryOff = getString(R.string.pref_gps_only_summary_gps_and_network) preferenceGpsOnly.summaryOff = getString(R.string.pref_gps_only_summary_gps_and_network)
preferenceGpsOnly.setDefaultValue(false) preferenceGpsOnly.setDefaultValue(false)
preferenceCategoryGeneral.contains(preferenceGpsOnly)
screen.addPreference(preferenceGpsOnly)
// set up "Use Imperial Measurements" preference // set up "Use Imperial Measurements" preference
val preferenceImperialMeasurementUnits: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context) val preferenceImperialMeasurementUnits: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
@ -83,6 +90,8 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceImperialMeasurementUnits.summaryOn = getString(R.string.pref_imperial_measurement_units_summary_imperial) preferenceImperialMeasurementUnits.summaryOn = getString(R.string.pref_imperial_measurement_units_summary_imperial)
preferenceImperialMeasurementUnits.summaryOff = getString(R.string.pref_imperial_measurement_units_summary_metric) preferenceImperialMeasurementUnits.summaryOff = getString(R.string.pref_imperial_measurement_units_summary_metric)
preferenceImperialMeasurementUnits.setDefaultValue(LengthUnitHelper.useImperialUnits()) preferenceImperialMeasurementUnits.setDefaultValue(LengthUnitHelper.useImperialUnits())
preferenceCategoryGeneral.contains(preferenceImperialMeasurementUnits)
screen.addPreference(preferenceImperialMeasurementUnits)
// set up "App Theme" preference // set up "App Theme" preference
val preferenceThemeSelection: ListPreference = ListPreference(activity as Context) val preferenceThemeSelection: ListPreference = ListPreference(activity as Context)
@ -101,16 +110,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
return@setOnPreferenceChangeListener false return@setOnPreferenceChangeListener false
} }
} }
screen.addPreference(preferenceThemeSelection)
// set up "Delete Non-Starred" preference
val preferenceDeleteNonStarred: Preference = Preference(activity as Context)
preferenceDeleteNonStarred.title = getString(R.string.pref_delete_non_starred_title)
preferenceDeleteNonStarred.setIcon(R.drawable.ic_delete_24dp)
preferenceDeleteNonStarred.summary = getString(R.string.pref_delete_non_starred_summary)
preferenceDeleteNonStarred.setOnPreferenceClickListener{
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_DELETE_NON_STARRED, message = R.string.dialog_yes_no_message_delete_non_starred, yesButton = R.string.dialog_yes_no_positive_button_delete_non_starred)
return@setOnPreferenceClickListener true
}
// set up "Recording Accuracy" preference // set up "Recording Accuracy" preference
val DEFAULT_OMIT_RESTS = true val DEFAULT_OMIT_RESTS = true
@ -122,46 +122,27 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceOmitRests.summaryOn = getString(R.string.pref_omit_rests_on) preferenceOmitRests.summaryOn = getString(R.string.pref_omit_rests_on)
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)
preferenceCategoryGeneral.contains(preferenceOmitRests)
screen.addPreference(preferenceOmitRests)
val preferenceAutoExportInterval: SeekBarPreference = SeekBarPreference(activity as Context) val preferenceDeviceID: EditTextPreference = EditTextPreference(activity as Context)
preferenceAutoExportInterval.title = getString(R.string.pref_auto_export_interval_title) preferenceDeviceID.title = getString(R.string.pref_device_id)
preferenceAutoExportInterval.setIcon(R.drawable.ic_bar_chart_24) preferenceDeviceID.setIcon(R.drawable.ic_smartphone_24dp)
preferenceAutoExportInterval.key = Keys.PREF_AUTO_EXPORT_INTERVAL preferenceDeviceID.key = Keys.PREF_DEVICE_ID
preferenceAutoExportInterval.summary = getString(R.string.pref_auto_export_interval_summary) preferenceDeviceID.summary = getString(R.string.pref_device_id_summary) + "\n" + PreferencesHelper.load_device_id()
preferenceAutoExportInterval.showSeekBarValue = true preferenceDeviceID.setDefaultValue(random_int().toString())
preferenceAutoExportInterval.min = 1 preferenceCategoryGeneral.contains(preferenceDeviceID)
preferenceAutoExportInterval.max = 24 screen.addPreference(preferenceDeviceID)
preferenceAutoExportInterval.setDefaultValue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
// set up "Altitude Smoothing" preference val preferenceCategoryAbout: PreferenceCategory = PreferenceCategory(context)
// val preferenceAltitudeSmoothingValue: SeekBarPreference = SeekBarPreference(activity as Context) preferenceCategoryAbout.title = getString(R.string.pref_about_title)
// preferenceAltitudeSmoothingValue.title = getString(R.string.pref_altitude_smoothing_value_title) screen.addPreference(preferenceCategoryAbout)
// preferenceAltitudeSmoothingValue.setIcon(R.drawable.ic_bar_chart_24)
// preferenceAltitudeSmoothingValue.key = Keys.PREF_ALTITUDE_SMOOTHING_VALUE
// preferenceAltitudeSmoothingValue.summary = getString(R.string.pref_altitude_smoothing_value_summary)
// preferenceAltitudeSmoothingValue.showSeekBarValue = true
// preferenceAltitudeSmoothingValue.min = Keys.MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION
// preferenceAltitudeSmoothingValue.max = Keys.MAX_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION
// preferenceAltitudeSmoothingValue.setDefaultValue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
// set up "Reset" preference
val preferenceResetAdvanced: Preference = Preference(activity as Context)
preferenceResetAdvanced.title = getString(R.string.pref_reset_advanced_title)
preferenceResetAdvanced.setIcon(R.drawable.ic_undo_24dp)
preferenceResetAdvanced.summary = getString(R.string.pref_reset_advanced_summary)
preferenceResetAdvanced.setOnPreferenceClickListener{
preferenceOmitRests.isChecked = DEFAULT_OMIT_RESTS
// preferenceAltitudeSmoothingValue.value = Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE
return@setOnPreferenceClickListener true
}
// set up "App Version" preference // set up "App Version" preference
val preferenceAppVersion: Preference = Preference(context) val preferenceAppVersion: Preference = Preference(context)
preferenceAppVersion.title = getString(R.string.pref_app_version_title) preferenceAppVersion.title = getString(R.string.pref_app_version_title)
preferenceAppVersion.setIcon(R.drawable.ic_info_24dp) preferenceAppVersion.setIcon(R.drawable.ic_info_24dp)
preferenceAppVersion.summary = "${getString(R.string.pref_app_version_summary)} ${BuildConfig.VERSION_NAME} (${getString( preferenceAppVersion.summary = "${getString(R.string.pref_app_version_summary)} ${BuildConfig.VERSION_NAME}"
R.string.app_version_name)})"
preferenceAppVersion.setOnPreferenceClickListener { preferenceAppVersion.setOnPreferenceClickListener {
// copy to clipboard // copy to clipboard
val clip: ClipData = ClipData.newPlainText("simple text", preferenceAppVersion.summary) val clip: ClipData = ClipData.newPlainText("simple text", preferenceAppVersion.summary)
@ -170,40 +151,10 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
Toast.makeText(activity as Context, R.string.toast_message_copied_to_clipboard, Toast.LENGTH_LONG).show() Toast.makeText(activity as Context, R.string.toast_message_copied_to_clipboard, Toast.LENGTH_LONG).show()
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }
// set preference categories
val preferenceCategoryGeneral: PreferenceCategory = PreferenceCategory(activity as Context)
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
preferenceCategoryGeneral.contains(preferenceImperialMeasurementUnits)
preferenceCategoryGeneral.contains(preferenceGpsOnly)
val preferenceCategoryMaintenance: PreferenceCategory = PreferenceCategory(activity as Context)
preferenceCategoryMaintenance.title = getString(R.string.pref_maintenance_title)
preferenceCategoryMaintenance.contains(preferenceDeleteNonStarred)
val preferenceCategoryAdvanced: PreferenceCategory = PreferenceCategory(activity as Context)
preferenceCategoryAdvanced.title = getString(R.string.pref_advanced_title)
preferenceCategoryAdvanced.contains(preferenceOmitRests)
// preferenceCategoryAdvanced.contains(preferenceAltitudeSmoothingValue)
preferenceCategoryAdvanced.contains(preferenceResetAdvanced)
val preferenceCategoryAbout: PreferenceCategory = PreferenceCategory(context)
preferenceCategoryAbout.title = getString(R.string.pref_about_title)
preferenceCategoryAbout.contains(preferenceAppVersion) preferenceCategoryAbout.contains(preferenceAppVersion)
screen.addPreference(preferenceAppVersion)
// setup preference screen // setup preference screen
screen.addPreference(preferenceCategoryGeneral)
screen.addPreference(preferenceGpsOnly)
screen.addPreference(preferenceImperialMeasurementUnits)
screen.addPreference(preferenceThemeSelection)
screen.addPreference(preferenceCategoryMaintenance)
screen.addPreference(preferenceDeleteNonStarred)
screen.addPreference(preferenceCategoryAdvanced)
screen.addPreference(preferenceOmitRests)
// screen.addPreference(preferenceAltitudeSmoothingValue)
screen.addPreference(preferenceAutoExportInterval)
screen.addPreference(preferenceResetAdvanced)
screen.addPreference(preferenceCategoryAbout)
screen.addPreference(preferenceAppVersion)
preferenceScreen = screen preferenceScreen = screen
} }
@ -212,25 +163,11 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) { override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
when (type) { when (type) {
Keys.DIALOG_DELETE_NON_STARRED -> { Keys.DIALOG_DELETE_NON_STARRED -> {
when (dialogResult) {
// user tapped delete
true -> {
deleteNonStarred(activity as Context)
}
}
} }
else -> { else -> {
super.onYesNoDialog(type, dialogResult, payload, payloadString) super.onYesNoDialog(type, dialogResult, payload, payloadString)
} }
} }
} }
/* Removes track and track files for given position - used by TracklistFragment */
private fun deleteNonStarred(context: Context) {
val tracklist: Tracklist = load_tracklist(context)
tracklist.delete_non_starred(context)
} }
}

View file

@ -31,25 +31,17 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import org.y20k.trackbook.core.Database
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
import org.y20k.trackbook.core.track_from_file
import org.y20k.trackbook.dialogs.RenameTrackDialog import org.y20k.trackbook.dialogs.RenameTrackDialog
import org.y20k.trackbook.helpers.DateTimeHelper
import org.y20k.trackbook.helpers.FileHelper
import org.y20k.trackbook.helpers.LogHelper 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.helpers.iso8601_format
import org.y20k.trackbook.ui.TrackFragmentLayoutHolder import org.y20k.trackbook.ui.TrackFragmentLayoutHolder
import java.io.File import java.text.SimpleDateFormat
import java.util.*
class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener, MapOverlayHelper.MarkerListener { class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener, MapOverlayHelper.MarkerListener {
@ -59,39 +51,24 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
/* Main class variables */ /* Main class variables */
private lateinit var layout: TrackFragmentLayoutHolder private lateinit var layout: TrackFragmentLayoutHolder
private lateinit var trackFileUriString: String
/* Overrides onCreate from Fragment */
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
trackFileUriString = arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String()
}
/* Overrides onCreateView from Fragment */ /* Overrides onCreateView from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// initialize layout // initialize layout
val track: Track val database: Database = (requireActivity().applicationContext as Trackbook).database
if (this::trackFileUriString.isInitialized && trackFileUriString.isNotBlank()) val track: Track = Track(
{ database=database,
track = track_from_file(activity as Context, Uri.parse(trackFileUriString).toFile()) device_id= this.requireArguments().getString(Keys.ARG_TRACK_DEVICE_ID, ""),
} else { start_time= iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_START_TIME)!!),
track = Track() stop_time=iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_STOP_TIME)!!),
} )
track.load_trkpts()
layout = TrackFragmentLayoutHolder(activity as Context, this as MapOverlayHelper.MarkerListener, inflater, container, track) layout = TrackFragmentLayoutHolder(activity as Context, this as MapOverlayHelper.MarkerListener, inflater, container, track)
// set up share button // set up share button
layout.shareButton.setOnClickListener { layout.shareButton.setOnClickListener {
openSaveGpxDialog() openSaveGpxDialog()
} }
// layout.shareButton.setOnLongClickListener {
// val v = (activity as Context).getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
// v.vibrate(50)
// shareGpxTrack()
// return@setOnLongClickListener true
// }
// 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}"
@ -110,14 +87,12 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
return layout.rootView return layout.rootView
} }
/* Overrides onResume from Fragment */ /* Overrides onResume from Fragment */
override fun onResume() override fun onResume()
{ {
super.onResume() super.onResume()
} }
/* Overrides onPause from Fragment */ /* Overrides onPause from Fragment */
override fun onPause() override fun onPause()
{ {
@ -131,38 +106,25 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
private val requestSaveGpxLauncher = registerForActivityResult(StartActivityForResult(), this::requestSaveGpxResult) private val requestSaveGpxLauncher = registerForActivityResult(StartActivityForResult(), this::requestSaveGpxResult)
/* Pass the activity result */
private fun requestSaveGpxResult(result: ActivityResult) private fun requestSaveGpxResult(result: ActivityResult)
{ {
// save GPX file to result file location if (result.resultCode != Activity.RESULT_OK || result.data == null)
if (result.resultCode == Activity.RESULT_OK && result.data != null)
{ {
val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri() return
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)
{ {
// copy file async (= fire & forget - no return value needed) return
CoroutineScope(Dispatchers.IO).launch {
FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri)
}
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()
}
}
} }
val outputsuccess: Uri? = layout.track.export_gpx(activity as Context, targetUri)
/* Overrides onRenameTrackDialog from RenameTrackDialog */ if (outputsuccess == null)
override fun onRenameTrackDialog(textInput: String)
{ {
// rename track async (= fire & forget - no return value needed) Toast.makeText(activity as Context, "failed to export for some reason", Toast.LENGTH_LONG).show()
CoroutineScope(Dispatchers.IO).launch { FileHelper.renameTrackSuspended(activity as Context, layout.track, textInput) } }
// update name in layout
layout.track.name = textInput
layout.trackNameView.text = textInput
} }
/* Overrides onYesNoDialog from YesNoDialogListener */ /* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String)
@ -175,15 +137,18 @@ 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.id) // 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)
}
else ->
{
;
} }
} }
} }
} }
} }
/* Overrides onMarkerTapped from MarkerListener */ /* Overrides onMarkerTapped from MarkerListener */
override fun onMarkerTapped(latitude: Double, longitude: Double) override fun onMarkerTapped(latitude: Double, longitude: Double)
{ {
@ -192,12 +157,11 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
layout.updateTrackOverlay() layout.updateTrackOverlay()
} }
/* Opens up a file picker to select the save location */ /* Opens up a file picker to select the save location */
private fun openSaveGpxDialog() private fun openSaveGpxDialog()
{ {
val context = this.activity as Context val context = this.activity as Context
val export_name: String = DateTimeHelper.convertToSortableDateString(layout.track.recordingStart) + Keys.GPX_FILE_EXTENSION val export_name: String = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(layout.track.start_time) + Keys.GPX_FILE_EXTENSION
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
type = Keys.MIME_TYPE_GPX type = Keys.MIME_TYPE_GPX
@ -213,42 +177,5 @@ 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()
}
/* Share track as GPX via share sheet */
private fun shareGpxTrack()
{
val gpxFile = layout.track.get_gpx_file(this.activity as Context)
val gpxShareUri = FileProvider.getUriForFile(this.activity as Context, "${requireActivity().applicationContext.packageName}.provider", gpxFile)
val shareIntent: Intent = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND
data = gpxShareUri
type = Keys.MIME_TYPE_GPX
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
putExtra(Intent.EXTRA_STREAM, gpxShareUri)
}, null)
// show share sheet - if file helper is available
val packageManager: PackageManager? = activity?.packageManager
if (packageManager != null && shareIntent.resolveActivity(packageManager) != null)
{
startActivity(shareIntent)
}
else
{
Toast.makeText(activity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
} }
} }
}

View file

@ -18,40 +18,100 @@
package org.y20k.trackbook package org.y20k.trackbook
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.Application import android.app.Application
import android.content.pm.PackageManager
import android.database.Cursor
import android.util.Log
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import org.y20k.trackbook.core.Database
import org.y20k.trackbook.core.Homepoint
import org.y20k.trackbook.core.Trkpt
import org.y20k.trackbook.helpers.AppThemeHelper 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
import org.y20k.trackbook.helpers.PreferencesHelper.initPreferences import org.y20k.trackbook.helpers.PreferencesHelper.initPreferences
import org.y20k.trackbook.helpers.iso8601
import org.y20k.trackbook.helpers.iso8601_format
import java.io.File
/* /*
* Trackbook.class * Trackbook.class
*/ */
class Trackbook: Application() { class Trackbook(): Application() {
val database: Database = Database()
val homepoints: ArrayList<Homepoint> = ArrayList()
override fun onCreate()
/* Define log tag */ {
private val TAG: String = LogHelper.makeLogTag(Trackbook::class.java)
/* Implements onCreate */
override fun onCreate() {
super.onCreate() super.onCreate()
LogHelper.v(TAG, "Trackbook application started.") LogHelper.v("VOUSSOIR", "Trackbook application started.")
DynamicColors.applyToActivitiesIfAvailable(this) DynamicColors.applyToActivitiesIfAvailable(this)
// initialize single sharedPreferences object when app is launched // initialize single sharedPreferences object when app is launched
initPreferences() initPreferences()
// set Dark / Light theme state // set Dark / Light theme state
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection()) AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
ContextCompat.checkSelfPermission(applicationContext, android.Manifest.permission.ACCESS_COARSE_LOCATION)
ContextCompat.checkSelfPermission(applicationContext, android.Manifest.permission.ACCESS_FINE_LOCATION)
Log.i("VOUSSOIR", "Device ID = ${PreferencesHelper.load_device_id()}")
if (ContextCompat.checkSelfPermission(applicationContext, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
{
this.database.connect(File("/storage/emulated/0/Syncthing/GPX/trkpt_${PreferencesHelper.load_device_id()}.db"))
}
} }
fun load_homepoints()
{
this.homepoints.clear()
homepoint_generator().forEach { homepoint -> this.homepoints.add(homepoint) }
}
/* Implements onTerminate */ fun homepoint_generator() = iterator<Homepoint>
override fun onTerminate() { {
if (! database.ready)
{
return@iterator
}
val cursor: Cursor = database.connection.query(
"homepoints",
arrayOf("lat", "lon", "radius", "name"),
null,
null,
null,
null,
null,
null,
)
val COLUMN_LAT = cursor.getColumnIndex("lat")
val COLUMN_LON = cursor.getColumnIndex("lon")
val COLUMN_RADIUS = cursor.getColumnIndex("radius")
val COLUMN_NAME = cursor.getColumnIndex("name")
try
{
while (cursor.moveToNext())
{
val homepoint = Homepoint(
latitude=cursor.getDouble(COLUMN_LAT),
longitude=cursor.getDouble(COLUMN_LON),
radius=cursor.getDouble(COLUMN_RADIUS),
name=cursor.getString(COLUMN_NAME),
)
yield(homepoint)
}
}
finally
{
cursor.close();
}
}
override fun onTerminate()
{
super.onTerminate() super.onTerminate()
LogHelper.v(TAG, "Trackbook application terminated.") LogHelper.v("VOUSSOIR", "Trackbook application terminated.")
database.close()
} }
} }

View file

@ -31,17 +31,17 @@ 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.Manifest
import android.content.ContentValues
import android.os.* import android.os.*
import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import java.util.* import java.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.Runnable import kotlinx.coroutines.Runnable
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
import org.y20k.trackbook.core.load_temp_track import org.y20k.trackbook.core.Database
import org.y20k.trackbook.core.Trkpt
import org.y20k.trackbook.helpers.* import org.y20k.trackbook.helpers.*
import java.text.SimpleDateFormat
/* /*
* TrackerService class * TrackerService class
@ -52,24 +52,25 @@ class TrackerService: Service(), SensorEventListener
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_STOPPED
var gpsProviderActive: Boolean = false var gpsProviderActive: Boolean = false
var networkProviderActive: Boolean = false var networkProviderActive: Boolean = false
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 device_id: String = random_int().toString()
var recording_started: Date = GregorianCalendar.getInstance().time
var commitInterval: Int = Keys.COMMIT_INTERVAL
var currentBestLocation: Location = LocationHelper.getDefaultLocation() var currentBestLocation: Location = LocationHelper.getDefaultLocation()
var lastTempSave: Date = Keys.DEFAULT_DATE var lastCommit: Date = Keys.DEFAULT_DATE
var lastAutoExport: Date = Keys.DEFAULT_DATE
var stepCountOffset: Float = 0f var stepCountOffset: Float = 0f
var track: Track = Track() lateinit var track: Track
var gpsLocationListenerRegistered: Boolean = false var gpsLocationListenerRegistered: Boolean = false
var networkLocationListenerRegistered: Boolean = false var networkLocationListenerRegistered: Boolean = false
var bound: Boolean = false var bound: Boolean = false
private val binder = LocalBinder() private val binder = LocalBinder()
private val handler: Handler = Handler(Looper.getMainLooper()) private val handler: Handler = Handler(Looper.getMainLooper())
private var altitudeValues: SimpleMovingAverageQueue = SimpleMovingAverageQueue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE) lateinit var trackbook: Trackbook
private lateinit var locationManager: LocationManager private lateinit var locationManager: LocationManager
private lateinit var sensorManager: SensorManager private lateinit var sensorManager: SensorManager
private lateinit var notificationManager: NotificationManager private lateinit var notificationManager: NotificationManager
@ -111,7 +112,8 @@ 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()
{
if (gpsOnly) if (gpsOnly)
{ {
LogHelper.v(TAG, "Skipping Network listener. User prefers GPS-only.") LogHelper.v(TAG, "Skipping Network listener. User prefers GPS-only.")
@ -150,12 +152,11 @@ class TrackerService: Service(), SensorEventListener
fun clearTrack() fun clearTrack()
{ {
track = Track() trackingState = Keys.STATE_TRACKING_STOPPED
stepCountOffset = 0f
FileHelper.delete_temp_file(this as Context)
trackingState = Keys.STATE_TRACKING_NOT_STARTED
PreferencesHelper.saveTrackingState(trackingState) PreferencesHelper.saveTrackingState(trackingState)
stopForeground(true) track.delete()
track = Track(trackbook.database, device_id, start_time=GregorianCalendar.getInstance().time, stop_time=Date(GregorianCalendar.getInstance().time.time + 86400))
stopForeground(STOP_FOREGROUND_REMOVE)
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
} }
@ -205,9 +206,7 @@ class TrackerService: Service(), SensorEventListener
private fun displayNotification(): Notification { private fun displayNotification(): Notification {
val notification: Notification = notificationHelper.createNotification( val notification: Notification = notificationHelper.createNotification(
trackingState, trackingState,
track.distance, iso8601(GregorianCalendar.getInstance().time)
track.duration,
useImperial
) )
notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification) notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification)
return notification return notification
@ -233,11 +232,14 @@ class TrackerService: Service(), SensorEventListener
override fun onCreate() override fun onCreate()
{ {
super.onCreate() super.onCreate()
trackbook = (applicationContext as Trackbook)
trackbook.load_homepoints()
gpsOnly = PreferencesHelper.loadGpsOnly() gpsOnly = PreferencesHelper.loadGpsOnly()
device_id = PreferencesHelper.load_device_id()
track = Track(trackbook.database, device_id, start_time=GregorianCalendar.getInstance().time, stop_time=Date(GregorianCalendar.getInstance().time.time + 86400))
useImperial = PreferencesHelper.loadUseImperialUnits() useImperial = PreferencesHelper.loadUseImperialUnits()
omitRests = PreferencesHelper.loadOmitRests() omitRests = PreferencesHelper.loadOmitRests()
autoExportInterval = PreferencesHelper.loadAutoExportInterval() commitInterval = PreferencesHelper.loadCommitInterval()
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
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -248,8 +250,6 @@ class TrackerService: Service(), SensorEventListener
networkLocationListener = createLocationListener() networkLocationListener = createLocationListener()
trackingState = PreferencesHelper.loadTrackingState() trackingState = PreferencesHelper.loadTrackingState()
currentBestLocation = LocationHelper.getLastKnownLocation(this) currentBestLocation = LocationHelper.getLastKnownLocation(this)
track = load_temp_track(this)
// altitudeValues.capacity = PreferencesHelper.loadAltitudeSmoothingValue()
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener) PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
} }
@ -282,13 +282,11 @@ class TrackerService: Service(), SensorEventListener
if (sensorEvent != null) { if (sensorEvent != null) {
if (stepCountOffset == 0f) { if (stepCountOffset == 0f) {
// store steps previously recorded by the system // store steps previously recorded by the system
stepCountOffset = (sensorEvent.values[0] - 1) - track.stepCount // subtract any steps recorded during this session in case the app was killed stepCountOffset = (sensorEvent.values[0] - 1) - 0 // subtract any steps recorded during this session in case the app was killed
} }
// calculate step count - subtract steps previously recorded // calculate step count - subtract steps previously recorded
steps = sensorEvent.values[0] - stepCountOffset steps = sensorEvent.values[0] - stepCountOffset
} }
// update step count in track
track.stepCount = steps
} }
/* Overrides onStartCommand from Service */ /* Overrides onStartCommand from Service */
@ -300,7 +298,7 @@ class TrackerService: Service(), SensorEventListener
if (trackingState == Keys.STATE_TRACKING_ACTIVE) if (trackingState == Keys.STATE_TRACKING_ACTIVE)
{ {
LogHelper.w(TAG, "Trackbook has been killed by the operating system. Trying to resume recording.") LogHelper.w(TAG, "Trackbook has been killed by the operating system. Trying to resume recording.")
resumeTracking() startTracking()
} }
} }
else if (intent.action == Keys.ACTION_STOP) else if (intent.action == Keys.ACTION_STOP)
@ -311,10 +309,6 @@ class TrackerService: Service(), SensorEventListener
{ {
startTracking() startTracking()
} }
else if (intent.action == Keys.ACTION_RESUME)
{
resumeTracking()
}
// START_STICKY is used for services that are explicitly started and stopped as needed // START_STICKY is used for services that are explicitly started and stopped as needed
return START_STICKY return START_STICKY
@ -358,42 +352,11 @@ class TrackerService: Service(), SensorEventListener
} }
} }
/* Resume tracking after stop/pause */
fun resumeTracking()
{
// load temp track - returns an empty track if there is no temp file.
track = load_temp_track(this)
if (track.wayPoints.isNotEmpty()) {
track.wayPoints.last().isStopOver = true
}
track.resumed = true
track.recordingPaused += (GregorianCalendar.getInstance().time.time - track.recordingStop.time)
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)
if (!stepCounterAvailable) { if (!stepCounterAvailable) {
LogHelper.w(TAG, "Pedometer sensor not available.") LogHelper.w(TAG, "Pedometer sensor not available.")
track.stepCount = -1f
} }
} }
@ -401,12 +364,11 @@ class TrackerService: Service(), SensorEventListener
{ {
addGpsLocationListener() addGpsLocationListener()
addNetworkLocationListener() addNetworkLocationListener()
// set up new track
if (newTrack) {
track = Track()
stepCountOffset = 0f
}
trackingState = Keys.STATE_TRACKING_ACTIVE trackingState = Keys.STATE_TRACKING_ACTIVE
if (newTrack)
{
this.recording_started = GregorianCalendar.getInstance().time
}
PreferencesHelper.saveTrackingState(trackingState) PreferencesHelper.saveTrackingState(trackingState)
startStepCounter() startStepCounter()
handler.postDelayed(periodicTrackUpdate, 0) handler.postDelayed(periodicTrackUpdate, 0)
@ -415,19 +377,16 @@ class TrackerService: Service(), SensorEventListener
fun pauseTracking() fun pauseTracking()
{ {
track.recordingStop = GregorianCalendar.getInstance().time trackbook.database.commit()
CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) }
trackingState = Keys.STATE_TRACKING_PAUSED trackingState = Keys.STATE_TRACKING_STOPPED
PreferencesHelper.saveTrackingState(trackingState) PreferencesHelper.saveTrackingState(trackingState)
altitudeValues.reset()
sensorManager.unregisterListener(this) sensorManager.unregisterListener(this)
handler.removeCallbacks(periodicTrackUpdate) handler.removeCallbacks(periodicTrackUpdate)
displayNotification() displayNotification()
stopForeground(false) stopForeground(STOP_FOREGROUND_DETACH)
} }
/* /*
@ -435,24 +394,25 @@ class TrackerService: Service(), SensorEventListener
*/ */
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) { when (key) {
// preference "Restrict to GPS" Keys.PREF_GPS_ONLY ->
Keys.PREF_GPS_ONLY -> { {
gpsOnly = PreferencesHelper.loadGpsOnly() gpsOnly = PreferencesHelper.loadGpsOnly()
when (gpsOnly) { when (gpsOnly) {
true -> removeNetworkLocationListener() true -> removeNetworkLocationListener()
false -> addNetworkLocationListener() false -> addNetworkLocationListener()
} }
} }
// preference "Use Imperial Measurements" Keys.PREF_USE_IMPERIAL_UNITS ->
Keys.PREF_USE_IMPERIAL_UNITS -> { {
useImperial = PreferencesHelper.loadUseImperialUnits() useImperial = PreferencesHelper.loadUseImperialUnits()
} }
// preference "Recording Accuracy" Keys.PREF_OMIT_RESTS ->
Keys.PREF_OMIT_RESTS -> { {
omitRests = PreferencesHelper.loadOmitRests() omitRests = PreferencesHelper.loadOmitRests()
} }
Keys.PREF_AUTO_EXPORT_INTERVAL -> { Keys.PREF_DEVICE_ID ->
autoExportInterval = PreferencesHelper.loadAutoExportInterval() {
device_id = PreferencesHelper.load_device_id()
} }
} }
} }
@ -470,84 +430,96 @@ class TrackerService: Service(), SensorEventListener
* End of inner class * End of inner class
*/ */
fun should_keep_point(location: Location): Boolean
{
if(! trackbook.database.ready)
{
Log.i("VOUSSOIR", "Omitting due to database not ready.")
return false
}
if (location.latitude == 0.0 || location.longitude == 0.0)
{
Log.i("VOUSSOIR", "Omitting due to 0,0 location.")
return false
}
if (! LocationHelper.isRecentEnough(location))
{
Log.i("VOUSSOIR", "Omitting due to not recent enough.")
return false
}
if (! LocationHelper.isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY))
{
Log.i("VOUSSOIR", "Omitting due to not accurate enough.")
return false
}
for (homepoint in trackbook.homepoints)
{
if (LocationHelper.calculateDistance(homepoint.location, location) < homepoint.radius)
{
Log.i("VOUSSOIR", "Omitting due to homepoint ${homepoint}.")
return false;
}
}
if (track.trkpts.isEmpty())
{
return true
}
if (! LocationHelper.isDifferentEnough(track.trkpts.last().toLocation(), location, omitRests))
{
Log.i("VOUSSOIR", "Omitting due to too close to previous.")
return false
}
return true
}
/* /*
* Runnable: Periodically track updates (if recording active) * Runnable: Periodically track updates (if recording active)
*/ */
private val periodicTrackUpdate: Runnable = object : Runnable private val periodicTrackUpdate: Runnable = object : Runnable
{ {
override fun run() { override fun run() {
// add waypoint to track - step count is continuously updated in onSensorChanged
val success = track.add_waypoint(currentBestLocation, omitRests, track.resumed)
val now: Date = GregorianCalendar.getInstance().time val now: Date = GregorianCalendar.getInstance().time
if (success) { val nowstr: String = iso8601(now)
track.resumed = false val trkpt: Trkpt = Trkpt(location=currentBestLocation)
Log.i("VOUSSOIR", "Processing point ${currentBestLocation.latitude}, ${currentBestLocation.longitude} ${nowstr}.")
// store previous smoothed altitude if (should_keep_point((currentBestLocation)))
val previousAltitude: Double = altitudeValues.getAverage() {
// put current altitude into queue val values = ContentValues().apply {
val currentBestLocationAltitude: Double = currentBestLocation.altitude put("device_id", device_id)
if (currentBestLocationAltitude != Keys.DEFAULT_ALTITUDE) altitudeValues.add(currentBestLocationAltitude) put("lat", trkpt.latitude)
// TODO remove put("lon", trkpt.longitude)
// uncomment to use test altitude values - useful if testing with an emulator put("time", nowstr)
//altitudeValues.add(getTestAltitude()) // TODO remove put("accuracy", trkpt.accuracy)
// TODO remove put("sat", trkpt.numberSatellites)
put("ele", trkpt.altitude)
// only start calculating elevation differences, if enough data has been added to queue put("star", 0)
if (altitudeValues.prepared) { }
// get current smoothed altitude if (! trackbook.database.connection.inTransaction())
val currentAltitude: Double = altitudeValues.getAverage() {
// calculate and store elevation differences trackbook.database.connection.beginTransaction()
track = LocationHelper.calculateElevationDifferences(currentAltitude, previousAltitude, track) }
// TODO remove trackbook.database.connection.insert("trkpt", null, values)
LogHelper.d(TAG, "Elevation Calculation || prev = $previousAltitude | curr = $currentAltitude | pos = ${track.positiveElevation} | neg = ${track.negativeElevation}") track.trkpts.add(trkpt)
// TODO remove if (track.trkpts.size > track.dequelimit)
{
track.trkpts.removeFirst()
} }
// save a temp track if (now.time - lastCommit.time > Keys.SAVE_TEMP_TRACK_INTERVAL)
if (now.time - lastTempSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) { {
lastTempSave = now if (trackbook.database.connection.inTransaction())
CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) } {
trackbook.database.commit()
} }
lastCommit = now
} }
if (now.time - track.recordingStart.time > (autoExportInterval * Keys.ONE_HOUR_IN_MILLISECONDS)) {
saveTrackAndStartNew(this@TrackerService)
} }
// update notification // update notification
displayNotification() displayNotification()
// re-run this in set interval // re-run this in set interval
handler.postDelayed(this, Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL) handler.postDelayed(this, Keys.TRACKING_INTERVAL)
} }
} }
/* /*
* End of declaration * 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>()
{
var prepared: Boolean = false
private var sum: Double = 0.0
override fun add(element: Double): Boolean
{
prepared = this.size + 1 >= Keys.MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION
if (this.size >= capacity) {
sum -= this.first
removeFirst()
}
sum += element
return super.add(element)
}
fun getAverage(): Double
{
return sum / this.size
}
fun reset()
{
this.clear()
prepared = false
sum = 0.0
}
}
} }

View file

@ -38,7 +38,7 @@ class TrackingToggleTileService: TileService() {
/* Main class variables */ /* Main class variables */
private var bound: Boolean = false private var bound: Boolean = false
private var trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED private var trackingState: Int = Keys.STATE_TRACKING_STOPPED
private lateinit var trackerService: TrackerService private lateinit var trackerService: TrackerService

View file

@ -37,6 +37,7 @@ import kotlinx.coroutines.Dispatchers.Main
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
import org.y20k.trackbook.helpers.LogHelper import org.y20k.trackbook.helpers.LogHelper
import org.y20k.trackbook.helpers.UiHelper import org.y20k.trackbook.helpers.UiHelper
import org.y20k.trackbook.helpers.iso8601_format
import org.y20k.trackbook.tracklist.TracklistAdapter import org.y20k.trackbook.tracklist.TracklistAdapter
@ -48,21 +49,18 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
/* Define log tag */ /* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TracklistFragment::class.java) private val TAG: String = LogHelper.makeLogTag(TracklistFragment::class.java)
/* Main class variables */ /* Main class variables */
private lateinit var tracklistAdapter: TracklistAdapter private lateinit var tracklistAdapter: TracklistAdapter
private lateinit var trackElementList: RecyclerView private lateinit var trackElementList: RecyclerView
private lateinit var tracklistOnboarding: ConstraintLayout private lateinit var tracklistOnboarding: ConstraintLayout
/* Overrides onCreateView from Fragment */ /* Overrides onCreateView from Fragment */
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// create tracklist adapter // create tracklist adapter
tracklistAdapter = TracklistAdapter(this) tracklistAdapter = TracklistAdapter(this, (requireActivity().applicationContext as Trackbook).database)
} }
/* Overrides onCreateView from Fragment */ /* Overrides onCreateView from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// find views // find views
@ -97,9 +95,9 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
override fun onTrackElementTapped(track: Track) { override fun onTrackElementTapped(track: Track) {
val bundle: Bundle = bundleOf( val bundle: Bundle = bundleOf(
Keys.ARG_TRACK_TITLE to track.name, Keys.ARG_TRACK_TITLE to track.name,
Keys.ARG_TRACK_FILE_URI to track.get_json_file(activity as Context).toUri().toString(), Keys.ARG_TRACK_DEVICE_ID to track.device_id,
Keys.ARG_GPX_FILE_URI to track.get_gpx_file(activity as Context).toUri().toString(), Keys.ARG_TRACK_START_TIME to iso8601_format.format(track.start_time),
Keys.ARG_TRACK_ID to track.id Keys.ARG_TRACK_STOP_TIME to iso8601_format.format(track.stop_time),
) )
findNavController().navigate(R.id.fragment_track, bundle) findNavController().navigate(R.id.fragment_track, bundle)
} }
@ -130,7 +128,6 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
} }
} }
// toggle onboarding layout // toggle onboarding layout
private fun toggleOnboardingLayout() { private fun toggleOnboardingLayout() {
when (tracklistAdapter.isEmpty()) { when (tracklistAdapter.isEmpty()) {
@ -147,13 +144,11 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
} }
} }
/* /*
* Inner class: custom LinearLayoutManager that overrides onLayoutCompleted * Inner class: custom LinearLayoutManager that overrides onLayoutCompleted
*/ */
inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false) { inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false)
{
override fun supportsPredictiveItemAnimations(): Boolean { override fun supportsPredictiveItemAnimations(): Boolean {
return true return true
} }

View file

@ -0,0 +1,52 @@
package org.y20k.trackbook.core
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.openOrCreateDatabase
import android.util.Log
import java.io.File
class Database()
{
var ready: Boolean = false
lateinit var file: File
lateinit var connection: SQLiteDatabase
fun close()
{
this.connection.close()
this.ready = false
}
fun connect(file: File)
{
this.file = file
this.connection = openOrCreateDatabase(file, null)
this.initialize_tables()
this.ready = true
}
fun commit()
{
if (! this.ready)
{
return
}
if (! this.connection.inTransaction())
{
return
}
Log.i("VOUSSOIR", "Committing.")
this.connection.setTransactionSuccessful()
this.connection.endTransaction()
}
private fun initialize_tables()
{
this.connection.beginTransaction()
this.connection.execSQL("CREATE TABLE IF NOT EXISTS meta(name TEXT PRIMARY KEY, value TEXT)")
this.connection.execSQL("CREATE TABLE IF NOT EXISTS trkpt(lat REAL NOT NULL, lon REAL NOT NULL, time TEXT NOT NULL, accuracy REAL, device_id INTEGER NOT NULL, ele INTEGER, sat INTEGER, star INTEGER, PRIMARY KEY(lat, lon, time, device_id))")
this.connection.execSQL("CREATE TABLE IF NOT EXISTS homepoints(lat REAL NOT NULL, lon REAL NOT NULL, radius REAL NOT NULL, name TEXT, PRIMARY KEY(lat, lon))")
this.connection.setTransactionSuccessful()
this.connection.endTransaction()
}
}

View file

@ -0,0 +1,20 @@
package org.y20k.trackbook.core
import android.location.Location
import java.util.*
class Homepoint(val latitude: Double, val longitude: Double, val radius: Double, val name: String)
{
val location: Location = this.to_location()
private fun to_location(): Location
{
val location: Location = Location("homepoint")
location.latitude = latitude
location.longitude = longitude
location.altitude = 0.0
location.accuracy = radius.toFloat()
location.time = GregorianCalendar.getInstance().time.time
return location
}
}

View file

@ -16,260 +16,78 @@
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.database.Cursor
import android.location.Location import android.location.Location
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Parcelable
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.Keep
import androidx.core.app.ActivityCompat
import androidx.core.net.toUri import androidx.core.net.toUri
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.Keys
import org.y20k.trackbook.R
import org.y20k.trackbook.helpers.DateTimeHelper import org.y20k.trackbook.helpers.DateTimeHelper
import org.y20k.trackbook.helpers.FileHelper
import org.y20k.trackbook.helpers.LocationHelper import org.y20k.trackbook.helpers.LocationHelper
import org.y20k.trackbook.helpers.iso8601
import org.y20k.trackbook.helpers.iso8601_format
import java.io.File import java.io.File
import java.net.URI
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
/* /*
* Track data class * Track data class
*/ */
@Keep
@Parcelize
data class Track ( data class Track (
@Expose val id: Long = make_random_id(), val database: Database,
@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION, val device_id: String,
@Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(), val start_time: Date,
@Expose var distance: Float = 0f, val stop_time: Date,
@Expose var duration: Long = 0L, var name: String = "",
@Expose var recordingPaused: Long = 0L, var dequelimit: Int = 7200,
@Expose var stepCount: Float = 0f, var view_latitude: Double = Keys.DEFAULT_LATITUDE,
@Expose var recordingStart: Date = GregorianCalendar.getInstance().time, var view_longitude: Double = Keys.DEFAULT_LONGITUDE,
@Expose var dateString: String = DateTimeHelper.convertToReadableDate(recordingStart), var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
@Expose var recordingStop: Date = recordingStart, val trkpts: ArrayDeque<Trkpt> = ArrayDeque<Trkpt>(dequelimit),
// The resumed flag will be true for the first point that is received after unpausing a var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL,
// 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 minAltitude: Double = 0.0,
@Expose var positiveElevation: Double = 0.0,
@Expose var negativeElevation: Double = 0.0,
@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 = DateTimeHelper.convertToReadableDate(recordingStart),
@Expose var starred: Boolean = false,
): Parcelable
{
fun add_waypoint(location: Location, omitRests: Boolean, resumed: Boolean): Boolean
{
// Step 1: Get previous location
val previousLocation: Location?
var numberOfWayPoints: Int = this.wayPoints.size
// CASE: First location
if (numberOfWayPoints == 0)
{
previousLocation = null
}
// CASE: Second location - check if first location was plausible & remove implausible location
else if (numberOfWayPoints == 1 && !LocationHelper.isFirstLocationPlausible(location, this))
{
previousLocation = null
numberOfWayPoints = 0
this.wayPoints.removeAt(0)
}
// CASE: Third location or second location (if first was plausible)
else
{
previousLocation = this.wayPoints[numberOfWayPoints - 1].toLocation()
}
// Step 2: Update duration
val now: Date = GregorianCalendar.getInstance().time
val difference: Long = now.time - this.recordingStop.time
this.duration += difference
this.recordingStop = now
// Step 3: Add waypoint, if recent and accurate and different enough
val shouldBeAdded: Boolean = (
LocationHelper.isRecentEnough(location) &&
LocationHelper.isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY) &&
LocationHelper.isDifferentEnough(previousLocation, location, omitRests)
) )
if (! shouldBeAdded)
{ {
return false fun delete()
}
// Step 3.1: Update distance (do not update if resumed -> we do not want to add values calculated during a recording pause)
if (!resumed)
{ {
this.distance = this.distance + LocationHelper.calculateDistance(previousLocation, location)
}
// Step 3.2: Update altitude values
val altitude: Double = location.altitude
if (altitude != 0.0)
{
if (numberOfWayPoints == 0)
{
this.maxAltitude = altitude
this.minAltitude = altitude
}
else
{
if (altitude > this.maxAltitude) this.maxAltitude = altitude
if (altitude < this.minAltitude) this.minAltitude = altitude
}
}
// Step 3.3: Toggle stop over status, if necessary
if (this.wayPoints.size < 0)
{
this.wayPoints[this.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location)
}
// Step 3.4: Add current location as point to center on for later display
this.latitude = location.latitude
this.longitude = location.longitude
// Step 3.5: Add location as new waypoint
this.wayPoints.add(WayPoint(location = location, distanceToStartingPoint = this.distance))
return true
}
fun delete(context: Context)
{
Log.i("VOUSSOIR", "Deleting track ${this.id}.")
val json_file: File = this.get_json_file(context)
if (json_file.isFile)
{
json_file.delete()
}
val gpx_file: File = this.get_gpx_file(context)
if (gpx_file.isFile)
{
gpx_file.delete()
}
} }
suspend fun delete_suspended(context: Context) suspend fun delete_suspended(context: Context)
{ {
return suspendCoroutine { cont -> return suspendCoroutine { cont ->
cont.resume(this.delete(context)) cont.resume(this.delete())
} }
} }
fun get_gpx_file(context: Context): File
{
val basename: String = this.id.toString() + Keys.GPX_FILE_EXTENSION
return File(context.getExternalFilesDir(Keys.FOLDER_GPX), basename)
}
fun get_export_gpx_file(context: Context): File fun get_export_gpx_file(context: Context): File
{ {
val basename: String = DateTimeHelper.convertToSortableDateString(this.recordingStart) + Keys.GPX_FILE_EXTENSION val basename: String = DateTimeHelper.convertToSortableDateString(this.start_time) + " " + DateTimeHelper.convertToSortableDateString(this.start_time) + Keys.GPX_FILE_EXTENSION
return File(File("/storage/emulated/0/Syncthing/GPX"), basename) return File(File("/storage/emulated/0/Syncthing/GPX"), basename)
} }
fun get_json_file(context: Context): File fun export_gpx(context: Context, fileuri: Uri): Uri?
{ {
val basename: String = this.id.toString() + Keys.TRACKBOOK_FILE_EXTENSION if (! database.ready)
return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), basename)
}
fun save_all_files(context: Context)
{ {
this.save_json(context) Log.i("VOUSSOIR", "Failed to export due to database not ready.")
this.save_gpx(context) return null
this.save_export_gpx(context)
} }
Log.i("VOUSSOIR", "Let's export to " + fileuri.toString())
suspend fun save_all_files_suspended(context: Context) val writer = context.contentResolver.openOutputStream(fileuri)
if (writer == null)
{ {
return suspendCoroutine { cont -> return null
cont.resume(this.save_all_files(context))
} }
}
fun save_gpx(context: Context)
{
val gpx: String = this.to_gpx()
FileHelper.write_text_file_noblank(gpx, this.get_gpx_file(context))
Log.i("VOUSSOIR", "Saved ${this.id}.gpx")
}
suspend fun save_gpx_suspended(context: Context)
{
return suspendCoroutine { cont ->
cont.resume(this.save_gpx(context))
}
}
fun save_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)
{
val json: String = this.to_json()
FileHelper.write_text_file_noblank(json, this.get_json_file(context))
Log.i("VOUSSOIR", "Saved ${this.id}.json")
}
suspend fun save_json_suspended(context: Context)
{
return suspendCoroutine { cont ->
cont.resume(this.save_json(context))
}
}
fun save_temp(context: Context)
{
val json: String = this.to_json()
FileHelper.write_text_file_noblank(json, FileHelper.get_temp_file(context))
}
suspend fun save_temp_suspended(context: Context)
{
return suspendCoroutine { cont ->
cont.resume(this.save_temp(context))
}
}
fun to_gpx(): String {
val gpxString = StringBuilder("")
// Header // Header
gpxString.appendLine(""" val write = {x: String -> writer.write(x.encodeToByteArray()); writer.write("\n".encodeToByteArray())}
write("""
<?xml version="1.0" encoding="UTF-8" standalone="no" ?> <?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<gpx <gpx
version="1.1" creator="Trackbook App (Android)" version="1.1" creator="Trackbook App (Android)"
@ -278,62 +96,140 @@ data class Track (
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
> >
""".trimIndent()) """.trimIndent())
gpxString.appendLine("\t<metadata>") write("\t<metadata>")
gpxString.appendLine("\t\t<name>Trackbook Recording: ${this.name}</name>") write("\t\t<name>Trackbook Recording: ${this.name}</name>")
gpxString.appendLine("\t</metadata>") write("\t\t<device>${this.device_id}</device>")
write("\t</metadata>")
// POIs
val poiList: List<WayPoint> = this.wayPoints.filter { it.starred }
poiList.forEach { poi ->
gpxString.appendLine("\t<wpt lat=\"${poi.latitude}\" lon=\"${poi.longitude}\">")
gpxString.appendLine("\t\t<name>Point of interest</name>")
gpxString.appendLine("\t\t<ele>${poi.altitude}</ele>")
gpxString.appendLine("\t</wpt>")
}
// TRK // TRK
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
dateFormat.timeZone = TimeZone.getTimeZone("UTC") dateFormat.timeZone = TimeZone.getTimeZone("UTC")
gpxString.appendLine("\t<trk>") write("\t<trk>")
gpxString.appendLine("\t\t<name>${this.name}</name>") write("\t\t<name>${this.name}</name>")
gpxString.appendLine("\t\t<trkseg>") write("\t\t<trkseg>")
this.wayPoints.forEach { wayPoint ->
gpxString.appendLine("\t\t\t<trkpt lat=\"${wayPoint.latitude}\" lon=\"${wayPoint.longitude}\">")
gpxString.appendLine("\t\t\t\t<ele>${wayPoint.altitude}</ele>")
gpxString.appendLine("\t\t\t\t<time>${dateFormat.format(Date(wayPoint.time))}</time>")
gpxString.appendLine("\t\t\t\t<sat>${wayPoint.numberSatellites}</sat>")
gpxString.appendLine("\t\t\t</trkpt>")
}
gpxString.appendLine("\t\t</trkseg>")
gpxString.appendLine("\t</trk>")
gpxString.appendLine("</gpx>")
return gpxString.toString() trkpt_generator().forEach { trkpt ->
write("\t\t\t<trkpt lat=\"${trkpt.latitude}\" lon=\"${trkpt.longitude}\">")
write("\t\t\t\t<ele>${trkpt.altitude}</ele>")
write("\t\t\t\t<time>${iso8601_format.format(trkpt.time)}</time>")
write("\t\t\t\t<sat>${trkpt.numberSatellites}</sat>")
write("\t\t\t</trkpt>")
} }
fun to_json(): String write("\t\t</trkseg>")
write("\t</trk>")
write("</gpx>")
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, fileuri.toString(), Toast.LENGTH_SHORT).show()
}
return fileuri
}
fun load_trkpts()
{ {
return FileHelper.getCustomGson().toJson(this) this.trkpts.clear()
trkpt_generator().forEach { trkpt -> this.trkpts.add(trkpt) }
if (this.trkpts.size > 0)
{
this.view_latitude = this.trkpts.first().latitude
this.view_longitude = this.trkpts.first().longitude
} }
} }
fun load_temp_track(context: Context): Track fun statistics(): TrackStatistics
{ {
return track_from_file(context, FileHelper.get_temp_file(context)) var first: Trkpt? = null
var last: Trkpt? = null
var previous: Trkpt? = null
val stats = TrackStatistics()
for (trkpt in trkpt_generator())
{
if (previous == null)
{
first = trkpt
previous = trkpt
stats.max_altitude = trkpt.altitude
stats.min_altitude = trkpt.altitude
continue
}
stats.distance += LocationHelper.calculateDistance(previous.toLocation(), trkpt.toLocation())
val ascentdiff = trkpt.altitude - previous.altitude
if (ascentdiff > 0)
{
stats.total_ascent += ascentdiff
}
else
{
stats.total_descent += ascentdiff
}
if (trkpt.altitude > stats.max_altitude)
{
stats.max_altitude = trkpt.altitude
}
if (trkpt.altitude < stats.min_altitude)
{
stats.min_altitude = trkpt.altitude
}
last = trkpt
}
if (first == null || last == null)
{
return stats
}
stats.duration = last.time.time - first.time.time
stats.velocity = stats.distance / stats.duration
return stats
} }
fun track_from_file(context: Context, file: File): Track fun trkpt_generator() = iterator<Trkpt>
{ {
// get JSON from text file val cursor: Cursor = database.connection.query(
val json: String = FileHelper.readTextFile(context, file) "trkpt",
if (json.isEmpty()) arrayOf("lat", "lon", "time", "ele", "accuracy", "sat"),
"device_id = ? AND time > ? AND time < ?",
arrayOf(device_id, iso8601(start_time), iso8601(stop_time)),
null,
null,
"time ASC",
null,
)
val COLUMN_LAT = cursor.getColumnIndex("lat")
val COLUMN_LON = cursor.getColumnIndex("lon")
val COLUMN_ELE = cursor.getColumnIndex("ele")
val COLUMN_SAT = cursor.getColumnIndex("sat")
val COLUMN_ACCURACY = cursor.getColumnIndex("accuracy")
val COLUMN_TIME = cursor.getColumnIndex("time")
try
{ {
return Track() while (cursor.moveToNext())
{
val trkpt: Trkpt = Trkpt(
provider="",
latitude=cursor.getDouble(COLUMN_LAT),
longitude=cursor.getDouble(COLUMN_LON),
altitude=cursor.getDouble(COLUMN_ELE),
accuracy=cursor.getFloat(COLUMN_ACCURACY),
time=iso8601_format.parse(cursor.getString(COLUMN_TIME)),
distanceToStartingPoint=0F,
numberSatellites=cursor.getInt(COLUMN_SAT),
)
yield(trkpt)
}
}
finally
{
cursor.close();
}
} }
return FileHelper.getCustomGson().fromJson(json, Track::class.java)
} }
fun make_random_id(): Long data class TrackStatistics(
{ var distance: Double = 0.0,
return (Random.nextBits(31).toLong() shl 32) + Random.nextBits(32) var duration: Long = 0,
} var velocity: Double = 0.0,
var total_ascent: Double = 0.0,
var total_descent: Double = 0.0,
var max_altitude: Double = 0.0,
var min_altitude: Double = 0.0,
)

View file

@ -1,95 +0,0 @@
/*
* Tracklist.kt
* Implements the Tracklist data class
* A Tracklist stores a list of Tracks
*
* This file is part of
* TRACKBOOK - Movement Recorder for Android
*
* Copyright (c) 2016-22 - Y20K.org
* Licensed under the MIT-License
* http://opensource.org/licenses/MIT
*
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
* https://github.com/osmdroid/osmdroid
*/
package org.y20k.trackbook.core
import android.content.Context
import android.os.Parcelable
import android.util.Log
import androidx.annotation.Keep
import com.google.gson.annotations.Expose
import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.parcelize.Parcelize
import org.y20k.trackbook.Keys
/*
* Tracklist data class
*/
@Keep
@Parcelize
data class Tracklist (
@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION,
@Expose val tracks: MutableList<Track> = mutableListOf<Track>()
): Parcelable
{
fun delete_non_starred(context: Context)
{
val to_delete: List<Track> = this.tracks.filter{! it.starred}
to_delete.forEach { track ->
if (!track.starred)
{
track.delete(context)
}
}
this.tracks.removeIf{! it.starred}
}
suspend fun delete_non_starred_suspended(context: Context)
{
return suspendCoroutine { cont ->
cont.resume(this.delete_non_starred(context))
}
}
fun get_total_distance(): Double
{
return this.tracks.sumOf {it.distance.toDouble()}
}
fun get_total_duration(): Long
{
return this.tracks.sumOf {it.duration}
}
fun deepCopy(): Tracklist
{
return Tracklist(tracklistFormatVersion, mutableListOf<Track>().apply { addAll(tracks) })
}
}
fun load_tracklist(context: Context): Tracklist {
Log.i("VOUSSOIR", "Loading tracklist.")
val folder = context.getExternalFilesDir("tracks")
val tracklist: Tracklist = Tracklist()
if (folder == null)
{
return tracklist
}
folder.walk().filter{ f: File -> f.isFile }.forEach{ json_file ->
val track = track_from_file(context, json_file)
tracklist.tracks.add(track)
}
tracklist.tracks.sortByDescending {it.recordingStart}
return tracklist
}
suspend fun load_tracklist_suspended(context: Context): Tracklist
{
return suspendCoroutine {cont -> cont.resume(load_tracklist(context))}
}

View file

@ -23,19 +23,19 @@ import androidx.annotation.Keep
import com.google.gson.annotations.Expose import com.google.gson.annotations.Expose
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.y20k.trackbook.helpers.LocationHelper import org.y20k.trackbook.helpers.LocationHelper
import java.util.*
/* /*
* WayPoint data class * WayPoint data class
*/ */
@Keep @Keep
@Parcelize @Parcelize
data class WayPoint(@Expose val provider: String, data class Trkpt(@Expose val provider: String,
@Expose val latitude: Double, @Expose val latitude: Double,
@Expose val longitude: Double, @Expose val longitude: Double,
@Expose val altitude: Double, @Expose val altitude: Double,
@Expose val accuracy: Float, @Expose val accuracy: Float,
@Expose val time: Long, @Expose val time: Date,
@Expose val distanceToStartingPoint: Float = 0f, @Expose val distanceToStartingPoint: Float = 0f,
@Expose val numberSatellites: Int = 0, @Expose val numberSatellites: Int = 0,
@Expose var isStopOver: Boolean = false, @Expose var isStopOver: Boolean = false,
@ -43,28 +43,16 @@ data class WayPoint(@Expose val provider: String,
/* Constructor using just Location */ /* Constructor using just Location */
constructor(location: Location) : this ( constructor(location: Location) : this (
provider=location.provider, provider=location.provider.toString(),
latitude=location.latitude, latitude=location.latitude,
longitude=location.longitude, longitude=location.longitude,
altitude=location.altitude, altitude=location.altitude,
accuracy=location.accuracy, accuracy=location.accuracy,
time=location.time, time=Date(location.time),
distanceToStartingPoint=0F, distanceToStartingPoint=0F,
numberSatellites=LocationHelper.getNumberOfSatellites(location), numberSatellites=LocationHelper.getNumberOfSatellites(location),
) )
/* Constructor using Location plus distanceToStartingPoint and numberSatellites */
constructor(location: Location, distanceToStartingPoint: Float) : this (
provider=location.provider,
latitude=location.latitude,
longitude=location.longitude,
altitude=location. altitude,
accuracy=location.accuracy,
time=location.time,
distanceToStartingPoint=distanceToStartingPoint,
numberSatellites=LocationHelper.getNumberOfSatellites(location),
)
/* Converts WayPoint into Location */ /* Converts WayPoint into Location */
fun toLocation(): Location { fun toLocation(): Location {
val location: Location = Location(provider) val location: Location = Location(provider)
@ -72,7 +60,7 @@ data class WayPoint(@Expose val provider: String,
location.longitude = longitude location.longitude = longitude
location.altitude = altitude location.altitude = altitude
location.accuracy = accuracy location.accuracy = accuracy
location.time = time location.time = this.time.time
return location return location
} }

View file

@ -38,31 +38,6 @@ object FileHelper {
/* Define log tag */ /* Define log tag */
private val TAG: String = LogHelper.makeLogTag(FileHelper::class.java) private val TAG: String = LogHelper.makeLogTag(FileHelper::class.java)
fun delete_temp_file(context: Context)
{
val temp: File = get_temp_file(context)
if (temp.isFile())
{
temp.delete()
}
}
fun get_temp_file(context: Context): File
{
return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE)
}
/* Suspend function: Wrapper for renameTrack */
suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) {
return suspendCoroutine { cont ->
track.name = newName
track.save_json(context)
track.save_gpx(context)
cont.resume(Unit)
}
}
/* 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 ->

View file

@ -96,15 +96,12 @@ object LengthUnitHelper {
/* Converts for the given unit System distance and duration values to a readable velocity string */ /* Converts for the given unit System distance and duration values to a readable velocity string */
fun convertToVelocityString(trackDuration: Long, trackRecordingPause: Long, trackLength: Float, useImperialUnits: Boolean = false) : String { fun convertToVelocityString(velocity: Double, useImperialUnits: Boolean = false) : String {
var speed: String = "0" var speed: String = "0"
// duration minus pause in seconds if (velocity > 0.0) {
val duration: Long = (trackDuration - trackRecordingPause) / 1000L
if (duration > 0L) {
// speed in km/h / mph // speed in km/h / mph
val velocity: Double = convertMetersPerSecond((trackLength / duration), useImperialUnits) val velocity: Double = convertMetersPerSecond(velocity, useImperialUnits)
// create readable speed string // create readable speed string
var bd: BigDecimal = BigDecimal.valueOf(velocity) var bd: BigDecimal = BigDecimal.valueOf(velocity)
bd = bd.setScale(1, RoundingMode.HALF_UP) bd = bd.setScale(1, RoundingMode.HALF_UP)
@ -119,7 +116,7 @@ object LengthUnitHelper {
/* Coverts meters per second to either km/h or mph */ /* Coverts meters per second to either km/h or mph */
fun convertMetersPerSecond(metersPerSecond: Float, useImperial: Boolean = false): Double { fun convertMetersPerSecond(metersPerSecond: Double, useImperial: Boolean = false): Double {
if (useImperial) { if (useImperial) {
// mph // mph
return metersPerSecond * 2.2369362920544 return metersPerSecond * 2.2369362920544

View file

@ -51,14 +51,6 @@ object LocationHelper {
return defaultLocation return defaultLocation
} }
/* Checks if a location is older than one minute */
fun isOldLocation(location: Location): Boolean {
// check how many milliseconds the given location is old
return GregorianCalendar.getInstance().time.time - location.time > Keys.SIGNIFICANT_TIME_DIFFERENCE
}
/* Tries to return the last location that the system has stored */ /* Tries to return the last location that the system has stored */
fun getLastKnownLocation(context: Context): Location { fun getLastKnownLocation(context: Context): Location {
// get last location that Trackbook has stored // get last location that Trackbook has stored
@ -117,7 +109,6 @@ object LocationHelper {
} }
} }
/* Checks if GPS location provider is available and enabled */ /* Checks if GPS location provider is available and enabled */
fun isGpsEnabled(locationManager: LocationManager): Boolean { fun isGpsEnabled(locationManager: LocationManager): Boolean {
if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) { if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) {
@ -156,27 +147,6 @@ object LocationHelper {
return isAccurate return isAccurate
} }
/* Checks if the first location of track is plausible */
fun isFirstLocationPlausible(secondLocation: Location, track: Track): Boolean {
// speed in km/h
val speed: Double = calculateSpeed(firstLocation = track.wayPoints[0].toLocation(), secondLocation = secondLocation, firstTimestamp = track.recordingStart.time, secondTimestamp = GregorianCalendar.getInstance().time.time)
// plausible = speed under 250 km/h
return speed < Keys.IMPLAUSIBLE_TRACK_START_SPEED
}
/* Calculates speed */
private fun calculateSpeed(firstLocation: Location, secondLocation: Location, firstTimestamp: Long, secondTimestamp: Long, useImperial: Boolean = false): Double {
// time difference in seconds
val timeDifference: Long = (secondTimestamp - firstTimestamp) / 1000L
// distance in meters
val distance: Float = calculateDistance(firstLocation, secondLocation)
// speed in either km/h (default) or mph
return LengthUnitHelper.convertMetersPerSecond(distance / timeDifference, useImperial)
}
/* Checks if given location is different enough compared to previous location */ /* Checks if given location is different enough compared to previous location */
fun isDifferentEnough(previousLocation: Location?, location: Location, omitRests: Boolean): Boolean { fun isDifferentEnough(previousLocation: Location?, location: Location, omitRests: Boolean): Boolean {
// check if previous location is (not) available // check if previous location is (not) available
@ -216,51 +186,6 @@ object LocationHelper {
return distance return distance
} }
/* Calculate elevation differences */
fun calculateElevationDifferencesOld(previousLocation: Location?, location: Location, track: Track): Pair<Double, Double> {
// store current values
var positiveElevation: Double = track.positiveElevation
var negativeElevation: Double = track.negativeElevation
if (previousLocation != null) {
// factor is bigger than 1 if the time stamp difference is larger than the movement recording interval
val timeDifferenceFactor: Long = (location.time - previousLocation.time) / Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL
// get elevation difference and sum it up
val altitudeDifference: Double = location.altitude - previousLocation.altitude
if (altitudeDifference > 0 && altitudeDifference < Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) {
positiveElevation = track.positiveElevation + altitudeDifference // upwards movement
}
if (altitudeDifference < 0 && altitudeDifference > -Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) {
negativeElevation = track.negativeElevation + altitudeDifference // downwards movement
}
}
return Pair(positiveElevation, negativeElevation)
}
/* Calculate elevation differences */
fun calculateElevationDifferences(currentAltitude: Double, previousAltitude: Double, track: Track): Track {
if (currentAltitude != Keys.DEFAULT_ALTITUDE && previousAltitude != Keys.DEFAULT_ALTITUDE) {
val altitudeDifference: Double = currentAltitude - previousAltitude
if (altitudeDifference > 0) {
track.positiveElevation += altitudeDifference // upwards movement
}
if (altitudeDifference < 0) {
track.negativeElevation += altitudeDifference // downwards movement
}
}
return track
}
/* Checks if given location is a stop over */
fun isStopOver(previousLocation: Location?, location: Location): Boolean {
if (previousLocation == null) return false
// check how many milliseconds the given locations are apart
return location.time - previousLocation.time > Keys.STOP_OVER_THRESHOLD
}
/* Get number of satellites from Location extras */ /* Get number of satellites from Location extras */
fun getNumberOfSatellites(location: Location): Int { fun getNumberOfSatellites(location: Location): Int {
val numberOfSatellites: Int val numberOfSatellites: Int

View file

@ -36,7 +36,7 @@ import org.osmdroid.views.overlay.simplefastpoint.SimplePointTheme
import org.y20k.trackbook.Keys import org.y20k.trackbook.Keys
import org.y20k.trackbook.R import org.y20k.trackbook.R
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
import org.y20k.trackbook.core.WayPoint import org.y20k.trackbook.core.Trkpt
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -58,10 +58,10 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
/* Creates icon overlay for current position (used in MapFragment) */ /* Creates icon overlay for current position (used in MapFragment) */
fun createMyLocationOverlay(context: Context, location: Location, trackingState: Int): ItemizedIconOverlay<OverlayItem> { fun createMyLocationOverlay(context: Context, location: Location, trackingState: Int): ItemizedIconOverlay<OverlayItem>
{
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>() val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>()
val locationIsOld:Boolean = LocationHelper.isOldLocation(location) val locationIsOld: Boolean = !(LocationHelper.isRecentEnough(location))
// create marker // create marker
val newMarker: Drawable val newMarker: Drawable
@ -83,7 +83,24 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
} }
// add marker to list of overlay items // add marker to list of overlay items
val overlayItem: OverlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider, location.time) val overlayItem: OverlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider.toString(), location.time)
overlayItem.setMarker(newMarker)
overlayItems.add(overlayItem)
// create and return overlay for current position
return createOverlay(context, overlayItems, enableStarring = false)
}
/* Creates icon overlay for current position (used in MapFragment) */
fun createHomepointOverlay(context: Context, location: Location): ItemizedIconOverlay<OverlayItem>
{
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>()
// create marker
val newMarker: Drawable = ContextCompat.getDrawable(context, R.drawable.ic_homepoint_24dp)!!
// add marker to list of overlay items
val overlayItem: OverlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider.toString(), location.time)
overlayItem.setMarker(newMarker) overlayItem.setMarker(newMarker)
overlayItems.add(overlayItem) overlayItems.add(overlayItem)
@ -93,13 +110,14 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
/* Creates icon overlay for track */ /* Creates icon overlay for track */
fun createTrackOverlay(context: Context, track: Track, trackingState: Int): SimpleFastPointOverlay { fun createTrackOverlay(context: Context, track: Track, trackingState: Int): SimpleFastPointOverlay
{
// get marker color // get marker color
val color = if (trackingState == Keys.STATE_TRACKING_ACTIVE) context.getColor(R.color.default_red) val color = if (trackingState == Keys.STATE_TRACKING_ACTIVE) context.getColor(R.color.default_red)
else context.getColor(R.color.default_blue) else context.getColor(R.color.default_blue)
// gather points for overlay // gather points for overlay
val points: MutableList<IGeoPoint> = mutableListOf() val points: MutableList<IGeoPoint> = mutableListOf()
track.wayPoints.forEach { wayPoint -> track.trkpts.forEach { wayPoint ->
val label: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(wayPoint.time)} | ${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(wayPoint.accuracy)} (${wayPoint.provider})" val label: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(wayPoint.time)} | ${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(wayPoint.accuracy)} (${wayPoint.provider})"
// only add normal points // only add normal points
if (!wayPoint.starred && !wayPoint.isStopOver) { if (!wayPoint.starred && !wayPoint.isStopOver) {
@ -133,45 +151,60 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
return overlay return overlay
} }
/* Creates overlay containing start, stop, stopover and starred markers for track */ /* Creates overlay containing start, stop, stopover and starred markers for track */
fun createSpecialMakersTrackOverlay(context: Context, track: Track, trackingState: Int, displayStartEndMarker: Boolean = false): ItemizedIconOverlay<OverlayItem> { fun createSpecialMakersTrackOverlay(context: Context, track: Track, trackingState: Int, displayStartEndMarker: Boolean = false): ItemizedIconOverlay<OverlayItem>
{
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>() val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>()
val trackingActive: Boolean = trackingState == Keys.STATE_TRACKING_ACTIVE val trackingActive: Boolean = trackingState == Keys.STATE_TRACKING_ACTIVE
val maxIndex: Int = track.wayPoints.size - 1 val maxIndex: Int = track.trkpts.size - 1
track.wayPoints.forEachIndexed { index: Int, wayPoint: WayPoint -> track.trkpts.forEachIndexed { index: Int, trkpt: Trkpt ->
var overlayItem: OverlayItem? = null var overlayItem: OverlayItem? = null
if (!trackingActive && index == 0 && displayStartEndMarker && wayPoint.starred) { if (!trackingActive && index == 0 && displayStartEndMarker && trkpt.starred)
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time) {
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_start_starred_blue_48dp)!!) overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_start_starred_blue_48dp)!!)
} else if (!trackingActive && index == 0 && displayStartEndMarker && !wayPoint.starred) { }
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time) else if (!trackingActive && index == 0 && displayStartEndMarker && !trkpt.starred)
{
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_start_blue_48dp)!!) overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_start_blue_48dp)!!)
} else if (!trackingActive && index == maxIndex && displayStartEndMarker && wayPoint.starred) { }
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time) else if (!trackingActive && index == maxIndex && displayStartEndMarker && trkpt.starred)
{
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_end_starred_blue_48dp)!!) overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_end_starred_blue_48dp)!!)
} else if (!trackingActive && index == maxIndex && displayStartEndMarker && !wayPoint.starred) { }
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time) else if (!trackingActive && index == maxIndex && displayStartEndMarker && !trkpt.starred)
{
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_end_blue_48dp)!!) overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_end_blue_48dp)!!)
} else if (!trackingActive && wayPoint.starred) { }
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time) else if (!trackingActive && trkpt.starred)
{
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_star_blue_24dp)!!) overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_star_blue_24dp)!!)
} else if (trackingActive && wayPoint.starred) { }
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time) else if (trackingActive && trkpt.starred)
{
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_star_red_24dp)!!) overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_star_red_24dp)!!)
} else if (wayPoint.isStopOver) { }
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time) else if (trkpt.isStopOver)
{
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!) overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!)
} }
// add overlay item, if it was created // add overlay item, if it was created
if (overlayItem != null) overlayItems.add(overlayItem) if (overlayItem != null)
{
overlayItems.add(overlayItem)
}
} }
// create and return overlay for current position // create and return overlay for current position
return createOverlay(context, overlayItems, enableStarring = true) return createOverlay(context, overlayItems, enableStarring = true)
} }
/* Creates a marker overlay item */ /* Creates a marker overlay item */
private fun createOverlayItem(context: Context, latitude: Double, longitude: Double, accuracy: Float, provider: String, time: Long): OverlayItem { private fun createOverlayItem(context: Context, latitude: Double, longitude: Double, accuracy: Float, provider: String, time: Long): OverlayItem {
val title: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(time)}" val title: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(time)}"
@ -183,7 +216,6 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
return item return item
} }
/* Creates an overlay */ /* Creates an overlay */
private fun createOverlay(context: Context, overlayItems: ArrayList<OverlayItem>, enableStarring: Boolean): ItemizedIconOverlay<OverlayItem> { private fun createOverlay(context: Context, overlayItems: ArrayList<OverlayItem>, enableStarring: Boolean): ItemizedIconOverlay<OverlayItem> {
return ItemizedIconOverlay<OverlayItem>(context, overlayItems, return ItemizedIconOverlay<OverlayItem>(context, overlayItems,

View file

@ -45,7 +45,7 @@ class NotificationHelper(private val trackerService: TrackerService) {
/* Creates notification */ /* Creates notification */
fun createNotification(trackingState: Int, trackLength: Float, duration: Long, useImperial: Boolean): Notification { fun createNotification(trackingState: Int, timestamp: String): Notification {
// create notification channel if necessary // create notification channel if necessary
if (shouldCreateNotificationChannel()) { if (shouldCreateNotificationChannel()) {
@ -56,7 +56,7 @@ class NotificationHelper(private val trackerService: TrackerService) {
val builder = NotificationCompat.Builder(trackerService, Keys.NOTIFICATION_CHANNEL_RECORDING) val builder = NotificationCompat.Builder(trackerService, Keys.NOTIFICATION_CHANNEL_RECORDING)
builder.setContentIntent(showActionPendingIntent) builder.setContentIntent(showActionPendingIntent)
builder.setSmallIcon(R.drawable.ic_notification_icon_small_24dp) builder.setSmallIcon(R.drawable.ic_notification_icon_small_24dp)
builder.setContentText(getContentString(trackerService, duration, trackLength, useImperial)) builder.setContentText(timestamp)
// add icon and actions for stop, resume and show // add icon and actions for stop, resume and show
when (trackingState) { when (trackingState) {
@ -77,22 +77,13 @@ class NotificationHelper(private val trackerService: TrackerService) {
} }
/* Build context text for notification builder */
private fun getContentString(context: Context, duration: Long, trackLength: Float, useImperial: Boolean): String {
return "${LengthUnitHelper.convertDistanceToString(trackLength, useImperial)}${DateTimeHelper.convertToReadableTime(context, duration)}"
}
/* Checks if notification channel should be created */ /* Checks if notification channel should be created */
private fun shouldCreateNotificationChannel() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists() private fun shouldCreateNotificationChannel() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists()
/* Checks if notification channel exists */ /* Checks if notification channel exists */
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun nowPlayingChannelExists() = notificationManager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null private fun nowPlayingChannelExists() = notificationManager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null
/* Create a notification channel */ /* Create a notification channel */
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() { private fun createNotificationChannel() {
@ -103,20 +94,24 @@ class NotificationHelper(private val trackerService: TrackerService) {
notificationManager.createNotificationChannel(notificationChannel) notificationManager.createNotificationChannel(notificationChannel)
} }
/* Notification pending intents */ /* Notification pending intents */
private val stopActionPendingIntent = PendingIntent.getService( private val stopActionPendingIntent = PendingIntent.getService(
trackerService,14, trackerService,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP),PendingIntent.FLAG_IMMUTABLE) 14,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP),
PendingIntent.FLAG_IMMUTABLE
)
private val resumeActionPendingIntent = PendingIntent.getService( private val resumeActionPendingIntent = PendingIntent.getService(
trackerService, 16, trackerService,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME),PendingIntent.FLAG_IMMUTABLE) 16,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME),
PendingIntent.FLAG_IMMUTABLE
)
private val showActionPendingIntent: PendingIntent? = TaskStackBuilder.create(trackerService).run { private val showActionPendingIntent: PendingIntent? = TaskStackBuilder.create(trackerService).run {
addNextIntentWithParentStack(Intent(trackerService, MainActivity::class.java)) addNextIntentWithParentStack(Intent(trackerService, MainActivity::class.java))
getPendingIntent(10, PendingIntent.FLAG_IMMUTABLE) getPendingIntent(10, PendingIntent.FLAG_IMMUTABLE)
} }
/* Notification actions */ /* Notification actions */
private val stopAction = NotificationCompat.Action( private val stopAction = NotificationCompat.Action(
R.drawable.ic_notification_action_stop_24dp, R.drawable.ic_notification_action_stop_24dp,

View file

@ -20,6 +20,7 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.util.Log
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.y20k.trackbook.Keys import org.y20k.trackbook.Keys
@ -43,6 +44,13 @@ object PreferencesHelper {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
} }
fun load_device_id(): String
{
val v = sharedPreferences.getString(Keys.PREF_DEVICE_ID, random_int().toString()).toString();
Log.i("VOUSSOIR", "Loaded device_id ${v}.")
return v
}
fun loadZoomLevel(): Double { fun loadZoomLevel(): Double {
return sharedPreferences.getDouble(Keys.PREF_MAP_ZOOM_LEVEL, Keys.DEFAULT_ZOOM_LEVEL) return sharedPreferences.getDouble(Keys.PREF_MAP_ZOOM_LEVEL, Keys.DEFAULT_ZOOM_LEVEL)
} }
@ -52,7 +60,7 @@ object PreferencesHelper {
} }
fun loadTrackingState(): Int { fun loadTrackingState(): Int {
return sharedPreferences.getInt(Keys.PREF_TRACKING_STATE, Keys.STATE_TRACKING_NOT_STARTED) return sharedPreferences.getInt(Keys.PREF_TRACKING_STATE, Keys.STATE_TRACKING_STOPPED)
} }
fun saveTrackingState(trackingState: Int) { fun saveTrackingState(trackingState: Int) {
@ -71,14 +79,10 @@ object PreferencesHelper {
return sharedPreferences.getBoolean(Keys.PREF_OMIT_RESTS, true) return sharedPreferences.getBoolean(Keys.PREF_OMIT_RESTS, true)
} }
fun loadAutoExportInterval(): Int { fun loadCommitInterval(): Int {
return sharedPreferences.getInt(Keys.PREF_AUTO_EXPORT_INTERVAL, Keys.DEFAULT_AUTO_EXPORT_INTERVAL) return sharedPreferences.getInt(Keys.PREF_COMMIT_INTERVAL, Keys.COMMIT_INTERVAL)
} }
// fun loadAltitudeSmoothingValue(): Int {
// return sharedPreferences.getInt(Keys.PREF_ALTITUDE_SMOOTHING_VALUE, Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
// }
/* Loads the state of a map */ /* Loads the state of a map */
fun loadCurrentBestLocation(): Location { fun loadCurrentBestLocation(): Location {
val provider: String = sharedPreferences.getString(Keys.PREF_CURRENT_BEST_LOCATION_PROVIDER, LocationManager.NETWORK_PROVIDER) ?: LocationManager.NETWORK_PROVIDER val provider: String = sharedPreferences.getString(Keys.PREF_CURRENT_BEST_LOCATION_PROVIDER, LocationManager.NETWORK_PROVIDER) ?: LocationManager.NETWORK_PROVIDER

View file

@ -18,7 +18,6 @@ package org.y20k.trackbook.helpers
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
import java.util.*
import org.y20k.trackbook.R import org.y20k.trackbook.R
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
@ -35,7 +34,7 @@ object TrackHelper {
/* 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)
{ {
track.wayPoints.forEach { waypoint -> track.trkpts.forEach { waypoint ->
if (waypoint.latitude == latitude && waypoint.longitude == longitude) { if (waypoint.latitude == latitude && waypoint.longitude == longitude) {
waypoint.starred = !waypoint.starred waypoint.starred = !waypoint.starred
when (waypoint.starred) { when (waypoint.starred) {

View file

@ -0,0 +1,22 @@
package org.y20k.trackbook.helpers
import java.text.SimpleDateFormat
import java.util.*
import kotlin.random.Random
val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US)
fun iso8601(datetime: Date): String
{
return iso8601_format.format(datetime)
}
fun random_long(): Long
{
return (Random.nextBits(31).toLong() shl 32) + Random.nextBits(32)
}
fun random_int(): Int
{
return Random.nextBits(31)
}

View file

@ -19,6 +19,7 @@ package org.y20k.trackbook.tracklist
import android.content.Context import android.content.Context
import android.database.Cursor
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -26,49 +27,72 @@ import android.view.ViewGroup
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import org.y20k.trackbook.Keys import org.y20k.trackbook.Keys
import org.y20k.trackbook.R import org.y20k.trackbook.R
import org.y20k.trackbook.core.Database
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
import org.y20k.trackbook.core.Tracklist
import org.y20k.trackbook.core.load_tracklist
import org.y20k.trackbook.helpers.* import org.y20k.trackbook.helpers.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/* /*
* TracklistAdapter class * TracklistAdapter class
*/ */
class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { class TracklistAdapter(val fragment: Fragment, val database: Database) : RecyclerView.Adapter<RecyclerView.ViewHolder>()
{
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TracklistAdapter::class.java)
/* Main class variables */ /* Main class variables */
private val context: Context = fragment.activity as Context private val context: Context = fragment.activity as Context
private lateinit var tracklistListener: TracklistAdapterListener private lateinit var tracklistListener: TracklistAdapterListener
private var useImperial: Boolean = PreferencesHelper.loadUseImperialUnits() private var useImperial: Boolean = PreferencesHelper.loadUseImperialUnits()
private var tracklist: Tracklist = Tracklist() val tracks: ArrayList<Track> = ArrayList<Track>()
/* Listener Interface */ /* Listener Interface */
interface TracklistAdapterListener { interface TracklistAdapterListener
{
fun onTrackElementTapped(track: Track) { } fun onTrackElementTapped(track: Track) { }
} }
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) override fun onAttachedToRecyclerView(recyclerView: RecyclerView)
{ {
tracklistListener = fragment as TracklistAdapterListener tracklistListener = fragment as TracklistAdapterListener
tracklist = load_tracklist(context) tracks.clear()
if (! database.ready)
{
return
}
val cursor: Cursor = database.connection.query(
"trkpt",
arrayOf("distinct strftime('%Y-%m-%d', time)", "device_id"),
null,
null,
null,
null,
"time DESC",
null,
)
try
{
val df: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US)
while (cursor.moveToNext())
{
val start_time: Date? = df.parse(cursor.getString(0) + "T00:00:00.000")
val stop_time: Date? = df.parse(cursor.getString(0) + "T23:59:59.999")
Log.i("VOUSSOIR", "TracklistAdapter prep track ${cursor.getString(0)}")
if (start_time != null && stop_time != null)
{
tracks.add(Track(database=database, device_id=cursor.getString(1), start_time=start_time, stop_time=stop_time))
}
}
}
finally
{
cursor.close();
}
} }
@ -81,14 +105,16 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/* Overrides getItemViewType */ /* Overrides getItemViewType */
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int
{
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
return tracklist.tracks.size {
return tracks.size
} }
@ -97,17 +123,10 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
{ {
val positionInTracklist: Int = position val positionInTracklist: Int = position
val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder
elementTrackViewHolder.trackNameView.text = tracklist.tracks[positionInTracklist].name elementTrackViewHolder.trackNameView.text = getTrackName(positionInTracklist)
elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist) elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist)
when (tracklist.tracks[positionInTracklist].starred) {
true -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp))
false -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp))
}
elementTrackViewHolder.trackElement.setOnClickListener { elementTrackViewHolder.trackElement.setOnClickListener {
tracklistListener.onTrackElementTapped(tracklist.tracks[positionInTracklist]) tracklistListener.onTrackElementTapped(tracks[positionInTracklist])
}
elementTrackViewHolder.starButton.setOnClickListener {
toggleStarred(it, positionInTracklist)
} }
} }
@ -115,16 +134,16 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/* Get track name for given position */ /* Get track name for given position */
fun getTrackName(positionInRecyclerView: Int): String fun getTrackName(positionInRecyclerView: Int): String
{ {
return tracklist.tracks[positionInRecyclerView].name return SimpleDateFormat("yyyy-MM-dd", Locale.US).format(tracks[positionInRecyclerView].start_time)
} }
fun delete_track_at_position(context: Context, index: Int) fun delete_track_at_position(context: Context, index: Int)
{ {
val track = tracklist.tracks[index] // val track = tracklist.tracks[index]
track.delete(context) // track.delete()
tracklist.tracks.remove(track) // tracklist.tracks.remove(track)
notifyItemRemoved(index) // notifyItemRemoved(index)
notifyItemRangeChanged(index, this.itemCount); // 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)
@ -136,79 +155,38 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
fun delete_track_by_id(context: Context, trackId: Long) fun delete_track_by_id(context: Context, trackId: Long)
{ {
val index: Int = tracklist.tracks.indexOfFirst {it.id == trackId} // val index: Int = tracklist.tracks.indexOfFirst {it.id == trackId}
if (index == -1) { // if (index == -1) {
return // return
} // }
tracklist.tracks[index].delete(context) // tracklist.tracks[index].delete()
tracklist.tracks.removeAt(index) // tracklist.tracks.removeAt(index)
notifyItemRemoved(index) // notifyItemRemoved(index)
notifyItemRangeChanged(index, this.itemCount); // notifyItemRangeChanged(index, this.itemCount);
} }
fun isEmpty(): Boolean { fun isEmpty(): Boolean
return tracklist.tracks.size == 0
}
/* Toggles the starred state of tracklist element - and saves tracklist */
private fun toggleStarred(view: View, position: Int) {
val starButton: ImageButton = view as ImageButton
if (tracklist.tracks[position].starred)
{ {
starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp)) return tracks.size == 0
tracklist.tracks[position].starred = false
}
else
{
starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp))
tracklist.tracks[position].starred = true
}
tracklist.tracks[position].save_json(context)
} }
/* Creates the track data string */ /* Creates the track data string */
private fun createTrackDataString(position: Int): String { private fun createTrackDataString(position: Int): String
val track: Track = tracklist.tracks[position] {
val track_duration_string = DateTimeHelper.convertToReadableTime(context, track.duration) val track: Track = tracks[position]
val trackDataString: String return "device: " + track.device_id
when (track.name == track.dateString) { // val track_duration_string = DateTimeHelper.convertToReadableTime(context, track.duration)
// CASE: no individual name set - exclude date // val trackDataString: String
true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)}${track_duration_string}" // if (track.name == track.dateString)
// CASE: no individual name set - include date // {
false -> trackDataString = "${track.dateString}${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)}${track_duration_string}" // trackDataString = "${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
// }
// else
// {
// trackDataString = "${track.dateString} • ${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
// }
// return trackDataString
} }
return trackDataString
}
/*
* Inner class: DiffUtil.Callback that determines changes in data - improves list performance
*/
private inner class DiffCallback(val oldList: Tracklist, val newList: Tracklist): DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList.tracks[oldItemPosition]
val newItem = newList.tracks[newItemPosition]
return oldItem.id == newItem.id
}
override fun getOldListSize(): Int {
return oldList.tracks.size
}
override fun getNewListSize(): Int {
return newList.tracks.size
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList.tracks[oldItemPosition]
val newItem = newList.tracks[newItemPosition]
return (oldItem.id == newItem.id) && (oldItem.distance == newItem.distance)
}
}
/*
* End of inner class
*/
/* /*
@ -218,8 +196,6 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
val trackElement: ConstraintLayout = elementTrackLayout.findViewById(R.id.track_element) val trackElement: ConstraintLayout = elementTrackLayout.findViewById(R.id.track_element)
val trackNameView: TextView = elementTrackLayout.findViewById(R.id.track_name) val trackNameView: TextView = elementTrackLayout.findViewById(R.id.track_name)
val trackDataView: TextView = elementTrackLayout.findViewById(R.id.track_data) val trackDataView: TextView = elementTrackLayout.findViewById(R.id.track_data)
val starButton: ImageButton = elementTrackLayout.findViewById(R.id.star_button)
} }
/* /*
* End of inner class * End of inner class

View file

@ -49,6 +49,7 @@ import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
import org.y20k.trackbook.Keys import org.y20k.trackbook.Keys
import org.y20k.trackbook.R import org.y20k.trackbook.R
import org.y20k.trackbook.Trackbook
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
import org.y20k.trackbook.helpers.* import org.y20k.trackbook.helpers.*
@ -66,17 +67,11 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
var userInteraction: Boolean = false var userInteraction: Boolean = false
val currentLocationButton: FloatingActionButton val currentLocationButton: FloatingActionButton
val mainButton: ExtendedFloatingActionButton val mainButton: ExtendedFloatingActionButton
val saveButton: FloatingActionButton
val clearButton: FloatingActionButton
private val additionalButtons: Group
private val mapView: MapView private val mapView: MapView
private val homepoint_overlays: ArrayList<ItemizedIconOverlay<OverlayItem>> = ArrayList()
private var currentPositionOverlay: ItemizedIconOverlay<OverlayItem> private var currentPositionOverlay: ItemizedIconOverlay<OverlayItem>
private var currentTrackOverlay: SimpleFastPointOverlay? private var currentTrackOverlay: SimpleFastPointOverlay?
private var currentTrackSpecialMarkerOverlay: ItemizedIconOverlay<OverlayItem>? private var currentTrackSpecialMarkerOverlay: ItemizedIconOverlay<OverlayItem>?
private val liveStatisticsDistanceView: MaterialTextView
private val liveStatisticsDistanceOutlineView: MaterialTextView
private val liveStatisticsDurationView: MaterialTextView
private val liveStatisticsDurationOutlineView: MaterialTextView
private val useImperial: Boolean = PreferencesHelper.loadUseImperialUnits() private val useImperial: Boolean = PreferencesHelper.loadUseImperialUnits()
private var locationErrorBar: Snackbar private var locationErrorBar: Snackbar
private var controller: IMapController private var controller: IMapController
@ -90,13 +85,6 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
mapView = rootView.findViewById(R.id.map) mapView = rootView.findViewById(R.id.map)
currentLocationButton = rootView.findViewById(R.id.location_button) currentLocationButton = rootView.findViewById(R.id.location_button)
mainButton = rootView.findViewById(R.id.main_button) mainButton = rootView.findViewById(R.id.main_button)
additionalButtons = rootView.findViewById(R.id.additional_buttons)
saveButton = rootView.findViewById(R.id.button_save)
clearButton = rootView.findViewById(R.id.button_clear)
liveStatisticsDistanceView = rootView.findViewById(R.id.live_statistics_distance)
liveStatisticsDistanceOutlineView = rootView.findViewById(R.id.live_statistics_distance_outline)
liveStatisticsDurationView = rootView.findViewById(R.id.live_statistics_duration)
liveStatisticsDurationOutlineView = rootView.findViewById(R.id.live_statistics_duration_outline)
locationErrorBar = Snackbar.make(mapView, String(), Snackbar.LENGTH_INDEFINITE) locationErrorBar = Snackbar.make(mapView, String(), Snackbar.LENGTH_INDEFINITE)
// basic map setup // basic map setup
@ -124,11 +112,8 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
compassOverlay.setCompassCenter((screen_width / densityScalingFactor) - 36f, 36f) compassOverlay.setCompassCenter((screen_width / densityScalingFactor) - 36f, 36f)
mapView.overlays.add(compassOverlay) mapView.overlays.add(compassOverlay)
// position the live statistics val app: Trackbook = (context.applicationContext as Trackbook)
(liveStatisticsDistanceView.layoutParams as ConstraintLayout.LayoutParams).apply { app.homepoint_generator().forEach { homepoint -> mapView.overlays.add(MapOverlayHelper(markerListener).createHomepointOverlay(context, homepoint.location))}
// topMargin = (12 * densityScalingFactor).toInt() + statusBarHeight // TODO uncomment when transparent status bar is re-implemented
topMargin = (12 * densityScalingFactor).toInt()
}
// add my location overlay // add my location overlay
currentPositionOverlay = MapOverlayHelper(markerListener).createMyLocationOverlay(context, startLocation, trackingState) currentPositionOverlay = MapOverlayHelper(markerListener).createMyLocationOverlay(context, startLocation, trackingState)
@ -178,7 +163,7 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
/* Mark current position on map */ /* Mark current position on map */
fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED) { fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_TRACKING_STOPPED) {
mapView.overlays.remove(currentPositionOverlay) mapView.overlays.remove(currentPositionOverlay)
currentPositionOverlay = MapOverlayHelper(markerListener).createMyLocationOverlay(context, location, trackingState) currentPositionOverlay = MapOverlayHelper(markerListener).createMyLocationOverlay(context, location, trackingState)
mapView.overlays.add(currentPositionOverlay) mapView.overlays.add(currentPositionOverlay)
@ -193,7 +178,7 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
if (currentTrackSpecialMarkerOverlay != null) { if (currentTrackSpecialMarkerOverlay != null) {
mapView.overlays.remove(currentTrackSpecialMarkerOverlay) mapView.overlays.remove(currentTrackSpecialMarkerOverlay)
} }
if (track.wayPoints.isNotEmpty()) { if (track.trkpts.isNotEmpty()) {
val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener) val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener)
currentTrackOverlay = mapOverlayHelper.createTrackOverlay(context, track, trackingState) currentTrackOverlay = mapOverlayHelper.createTrackOverlay(context, track, trackingState)
currentTrackSpecialMarkerOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, trackingState) currentTrackSpecialMarkerOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, trackingState)
@ -202,59 +187,20 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
} }
} }
/* Update live statics */
fun updateLiveStatics(distance: Float, duration: Long, trackingState: Int) {
// toggle visibility
if (trackingState == Keys.STATE_TRACKING_NOT_STARTED)
{
liveStatisticsDistanceView.isGone = true
liveStatisticsDurationView.isGone = true
liveStatisticsDistanceOutlineView.isGone = true
liveStatisticsDurationOutlineView.isGone = true
}
else
{
liveStatisticsDistanceView.isVisible = true
liveStatisticsDurationView.isVisible = true
liveStatisticsDistanceOutlineView.isVisible = true
liveStatisticsDurationOutlineView.isVisible = true
// update distance and duration (and add outline)
val distanceString: String = LengthUnitHelper.convertDistanceToString(distance, useImperial)
liveStatisticsDistanceView.text = distanceString
liveStatisticsDistanceOutlineView.text = distanceString
liveStatisticsDistanceOutlineView.paint.strokeWidth = 5f
liveStatisticsDistanceOutlineView.paint.style = Paint.Style.STROKE
val durationString: String = DateTimeHelper.convertToReadableTime(context, duration, compactFormat = true)
liveStatisticsDurationView.text = durationString
liveStatisticsDurationOutlineView.text = durationString
liveStatisticsDurationOutlineView.paint.strokeWidth = 5f
liveStatisticsDurationOutlineView.paint.style = Paint.Style.STROKE
}
}
/* Toggles state of main button and additional buttons (save & resume) */ /* Toggles state of main button and additional buttons (save & resume) */
fun updateMainButton(trackingState: Int) fun updateMainButton(trackingState: Int)
{ {
when (trackingState) { when (trackingState) {
Keys.STATE_TRACKING_NOT_STARTED -> { Keys.STATE_TRACKING_STOPPED -> {
mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp) mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp)
mainButton.text = context.getString(R.string.button_start) mainButton.text = context.getString(R.string.button_start)
mainButton.contentDescription = context.getString(R.string.descr_button_start) mainButton.contentDescription = context.getString(R.string.descr_button_start)
additionalButtons.isGone = true
currentLocationButton.isVisible = true currentLocationButton.isVisible = true
} }
Keys.STATE_TRACKING_ACTIVE -> { Keys.STATE_TRACKING_ACTIVE -> {
mainButton.setIconResource(R.drawable.ic_pause_24dp) mainButton.setIconResource(R.drawable.ic_fiber_manual_stop_24dp)
mainButton.text = context.getString(R.string.button_pause) mainButton.text = context.getString(R.string.button_pause)
mainButton.contentDescription = context.getString(R.string.descr_button_start) mainButton.contentDescription = context.getString(R.string.descr_button_pause)
additionalButtons.isGone = true
currentLocationButton.isVisible = true
}
Keys.STATE_TRACKING_PAUSED -> {
mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp)
mainButton.text = context.getString(R.string.button_resume)
mainButton.contentDescription = context.getString(R.string.descr_button_resume)
additionalButtons.isVisible = true
currentLocationButton.isVisible = true currentLocationButton.isVisible = true
} }
} }

View file

@ -30,9 +30,6 @@ import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.textview.MaterialTextView import com.google.android.material.textview.MaterialTextView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.osmdroid.api.IGeoPoint import org.osmdroid.api.IGeoPoint
import org.osmdroid.api.IMapController import org.osmdroid.api.IMapController
import org.osmdroid.events.MapListener import org.osmdroid.events.MapListener
@ -50,6 +47,7 @@ import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
import org.y20k.trackbook.Keys import org.y20k.trackbook.Keys
import org.y20k.trackbook.R import org.y20k.trackbook.R
import org.y20k.trackbook.core.Track import org.y20k.trackbook.core.Track
import org.y20k.trackbook.core.TrackStatistics
import org.y20k.trackbook.helpers.* import org.y20k.trackbook.helpers.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -58,12 +56,14 @@ import kotlin.math.roundToInt
* TrackFragmentLayoutHolder class * TrackFragmentLayoutHolder class
*/ */
//data class TrackFragmentLayoutHolder(private var context: Context, private var markerListener: MapOverlayHelper.MarkerListener, private var inflater: LayoutInflater, private var statusBarHeight: Int, private var container: ViewGroup?, var track: Track): MapListener { TODO REMOVE //data class TrackFragmentLayoutHolder(private var context: Context, private var markerListener: MapOverlayHelper.MarkerListener, private var inflater: LayoutInflater, private var statusBarHeight: Int, private var container: ViewGroup?, var track: Track): MapListener { TODO REMOVE
data class TrackFragmentLayoutHolder(private var context: Context, private var markerListener: MapOverlayHelper.MarkerListener, private var inflater: LayoutInflater, private var container: ViewGroup?, var track: Track): MapListener { data class TrackFragmentLayoutHolder(
private var context: Context,
/* Define log tag */ private var markerListener: MapOverlayHelper.MarkerListener,
private val TAG: String = LogHelper.makeLogTag(TrackFragmentLayoutHolder::class.java) private var inflater: LayoutInflater,
private var container: ViewGroup?,
var track: Track
): MapListener
{
/* Main class variables */ /* Main class variables */
val rootView: View val rootView: View
val shareButton: ImageButton val shareButton: ImageButton
@ -78,7 +78,6 @@ 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
@ -115,13 +114,12 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
mapView.setTileSource(TileSourceFactory.MAPNIK) mapView.setTileSource(TileSourceFactory.MAPNIK)
mapView.setMultiTouchControls(true) mapView.setMultiTouchControls(true)
mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER) mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
controller.setCenter(GeoPoint(track.latitude, track.longitude)) controller.setCenter(GeoPoint(track.view_latitude, track.view_longitude))
controller.setZoom(track.zoomLevel) controller.setZoom(track.zoomLevel)
// 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)
@ -156,9 +154,9 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
// create map overlay // create map overlay
val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener) val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener)
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED) trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED)
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED, displayStartEndMarker = true) trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED, displayStartEndMarker = true)
if (track.wayPoints.isNotEmpty()) { if (track.trkpts.isNotEmpty()) {
mapView.overlays.add(trackSpecialMarkersOverlay) mapView.overlays.add(trackSpecialMarkersOverlay)
mapView.overlays.add(trackOverlay) mapView.overlays.add(trackOverlay)
} }
@ -172,77 +170,48 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
/* Updates map overlay */ /* Updates map overlay */
fun updateTrackOverlay() { fun updateTrackOverlay()
{
if (trackOverlay != null) { if (trackOverlay != null) {
mapView.overlays.remove(trackOverlay) mapView.overlays.remove(trackOverlay)
} }
if (trackSpecialMarkersOverlay != null) { if (trackSpecialMarkersOverlay != null) {
mapView.overlays.remove(trackSpecialMarkersOverlay) mapView.overlays.remove(trackSpecialMarkersOverlay)
} }
if (track.wayPoints.isNotEmpty()) { if (track.trkpts.isNotEmpty()) {
val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener) val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener)
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED) trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED)
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED, displayStartEndMarker = true) trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED, displayStartEndMarker = true)
mapView.overlays.add(trackOverlay) mapView.overlays.add(trackOverlay)
mapView.overlays.add(trackSpecialMarkersOverlay) mapView.overlays.add(trackSpecialMarkersOverlay)
} }
// save track
CoroutineScope(Dispatchers.IO).launch { track.save_all_files_suspended(context) }
} }
/* 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.view_latitude != 0.0 && track.view_longitude != 0.0)
{ {
CoroutineScope(Dispatchers.IO).launch { track.save_json_suspended(context) }
} }
} }
/* Sets up the statistics sheet */ /* Sets up the statistics sheet */
private fun setupStatisticsViews() { private fun setupStatisticsViews()
// get step count string - hide step count if not available
val steps: String
if (track.stepCount == -1f)
{ {
steps = context.getString(R.string.statistics_sheet_p_steps_no_pedometer)
stepsTitleView.isGone = true
stepsView.isGone = true
}
else
{
steps = track.stepCount.roundToInt().toString()
stepsTitleView.isVisible = true
stepsView.isVisible = true
}
// populate views // populate views
val stats: TrackStatistics = track.statistics()
trackNameView.text = track.name trackNameView.text = track.name
trackidView.text = track.id.toString() distanceView.text = LengthUnitHelper.convertDistanceToString(stats.distance, useImperialUnits)
distanceView.text = LengthUnitHelper.convertDistanceToString(track.distance, useImperialUnits) waypointsView.text = track.trkpts.size.toString()
stepsView.text = steps durationView.text = DateTimeHelper.convertToReadableTime(context, stats.duration)
waypointsView.text = track.wayPoints.size.toString() velocityView.text = LengthUnitHelper.convertToVelocityString(stats.velocity, useImperialUnits)
durationView.text = DateTimeHelper.convertToReadableTime(context, track.duration) recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.start_time)
velocityView.text = LengthUnitHelper.convertToVelocityString(track.duration, track.recordingPaused, track.distance, useImperialUnits) recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.stop_time)
recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStart) maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.max_altitude, useImperialUnits)
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStop) minAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.min_altitude, useImperialUnits)
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits) positiveElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_ascent, useImperialUnits)
minAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.minAltitude, useImperialUnits) negativeElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_descent, useImperialUnits)
positiveElevationView.text = LengthUnitHelper.convertDistanceToString(track.positiveElevation, useImperialUnits)
negativeElevationView.text = LengthUnitHelper.convertDistanceToString(track.negativeElevation, useImperialUnits)
// show / hide recording pause
if (track.recordingPaused != 0L) {
recordingPausedLabelView.isVisible = true
recordingPausedView.isVisible = true
recordingPausedView.text = DateTimeHelper.convertToReadableTime(context, track.recordingPaused)
} else {
recordingPausedLabelView.isGone = true
recordingPausedView.isGone = true
}
// inform user about possible accuracy issues with altitude measurements // inform user about possible accuracy issues with altitude measurements
elevationDataViews.referencedIds.forEach { id -> elevationDataViews.referencedIds.forEach { id ->
@ -265,7 +234,6 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
} }
} }
/* Defines the behavior of the statistics sheet */ /* Defines the behavior of the statistics sheet */
private fun getStatisticsSheetCallback(): BottomSheetBehavior.BottomSheetCallback { private fun getStatisticsSheetCallback(): BottomSheetBehavior.BottomSheetCallback {
return object : BottomSheetBehavior.BottomSheetCallback() { return object : BottomSheetBehavior.BottomSheetCallback() {
@ -313,8 +281,8 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
return false return false
} else { } else {
val center: IGeoPoint = mapView.mapCenter val center: IGeoPoint = mapView.mapCenter
track.latitude = center.latitude track.view_latitude = center.latitude
track.longitude = center.longitude track.view_longitude = center.longitude
return true return true
} }
} }

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="36dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="36dp">
<path
android:fillColor="@color/recording_management_buttons_icon"
android:pathData="M5,5h14v14H5z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/homepoint"
android:pathData="M 12.01 2.5 L 5.883 6.936 L 5.883 3.604 L 3.674 3.604 L 3.674 8.535 L 2.01 9.74 L 1.99 21.5 L 12.35 21.5 L 12.35 16.047 L 17.215 16.047 L 17.215 21.5 L 22.01 21.5 L 22.01 9.74 Z"/>
</vector>

View file

@ -28,7 +28,6 @@
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="@color/text_default" android:textColor="@color/text_default"
app:layout_constraintBottom_toTopOf="@+id/track_data" app:layout_constraintBottom_toTopOf="@+id/track_data"
app:layout_constraintEnd_toStartOf="@+id/star_button"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@string/sample_text_track_name" /> tools:text="@string/sample_text_track_name" />
@ -37,28 +36,17 @@
android:id="@+id/track_data" android:id="@+id/track_data"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:ellipsize="end" android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/text_lightweight" android:textColor="@color/text_lightweight"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/track_name" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/track_name" app:layout_constraintStart_toStartOf="@+id/track_name"
tools:text="@string/sample_text_track_data" /> tools:text="@string/sample_text_track_data" />
<ImageButton
android:id="@+id/star_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:backgroundTint="@color/default_transparent"
android:contentDescription="@string/descr_mark_starred_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_star_outline_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View file

@ -22,40 +22,11 @@
app:layout_dodgeInsetEdges="bottom"> app:layout_dodgeInsetEdges="bottom">
<!-- BUTTON SAVE --> <!-- BUTTON SAVE -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/button_save"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/descr_button_save"
app:backgroundTint="@color/recording_management_buttons_background"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="@+id/main_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintStart_toEndOf="@+id/main_button"
app:layout_constraintTop_toTopOf="@+id/main_button"
app:srcCompat="@drawable/ic_save_24dp"
app:tint="@color/recording_management_buttons_icon" />
<!-- BUTTON CLEAR --> <!-- BUTTON CLEAR -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/button_clear"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/descr_button_delete"
app:backgroundTint="@color/recording_management_buttons_background"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="@+id/main_button"
app:layout_constraintEnd_toStartOf="@+id/main_button"
app:layout_constraintHorizontal_bias="0.85"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/main_button"
app:srcCompat="@drawable/ic_delete_24dp"
app:tint="@color/recording_management_buttons_icon" />
<!-- MAIN BUTTON --> <!-- MAIN BUTTON -->
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/main_button" android:id="@+id/main_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -76,69 +47,19 @@
android:id="@+id/location_button" android:id="@+id/location_button"
style="@style/Widget.MaterialComponents.FloatingActionButton" style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="56dp"
android:layout_marginTop="64dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/descr_button_location" android:contentDescription="@string/descr_button_location"
android:src="@drawable/ic_current_location_24dp" android:src="@drawable/ic_current_location_24dp"
app:backgroundTint="@color/location_button_background" app:backgroundTint="@color/location_button_background"
app:fabSize="mini" app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/location_button_icon" /> app:tint="@color/location_button_icon" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/live_statistics_distance_outline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="textEnd"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/text_outline_default"
app:layout_constraintEnd_toEndOf="@+id/live_statistics_distance"
app:layout_constraintTop_toTopOf="@+id/live_statistics_distance" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/live_statistics_distance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="textEnd"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/text_default"
app:layout_constraintEnd_toEndOf="@+id/live_statistics_duration"
app:layout_constraintTop_toBottomOf="@+id/live_statistics_duration"
tools:text="@string/sample_text_default_live_statistics_distance" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/live_statistics_duration_outline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="textEnd"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/text_outline_default"
app:layout_constraintEnd_toEndOf="@+id/live_statistics_duration"
app:layout_constraintTop_toTopOf="@+id/live_statistics_duration" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/live_statistics_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAlignment="textEnd"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/text_default"
app:layout_constraintEnd_toEndOf="@+id/location_button"
app:layout_constraintTop_toBottomOf="@+id/location_button"
tools:text="@string/sample_text_default_live_statistics_duration" />
<!-- GROUPS --> <!-- GROUPS -->
<androidx.constraintlayout.widget.Group
android:id="@+id/additional_buttons"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:constraint_referenced_ids="button_clear,button_save" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -74,30 +74,6 @@
app:layout_constraintTop_toTopOf="@+id/statistics_track_name_headline" app:layout_constraintTop_toTopOf="@+id/statistics_track_name_headline"
app:srcCompat="@drawable/ic_save_to_storage_24dp" /> app:srcCompat="@drawable/ic_save_to_storage_24dp" />
<com.google.android.material.textview.MaterialTextView
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_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 <com.google.android.material.textview.MaterialTextView
android:id="@+id/statistics_p_distance" android:id="@+id/statistics_p_distance"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -108,7 +84,7 @@
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_p_trackid" /> app:layout_constraintTop_toBottomOf="@+id/statistics_track_name_headline" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/statistics_data_distance" android:id="@+id/statistics_data_distance"

View file

@ -18,7 +18,6 @@
<!-- fab sub menu_bottom_navigation --> <!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Ryd</string> <string name="button_delete">Ryd</string>
<string name="button_save">Gem</string> <string name="button_save">Gem</string>
<string name="button_resume">Fortsæt</string>
<!-- dialogs --> <!-- dialogs -->
<string name="dialog_share_gpx">Del GPX fil med</string> <string name="dialog_share_gpx">Del GPX fil med</string>
<string name="dialog_error_empty_recording_message">Trackbook har ingen rutepunkter endnu.</string> <string name="dialog_error_empty_recording_message">Trackbook har ingen rutepunkter endnu.</string>
@ -86,9 +85,6 @@
<string name="pref_about_title">Om</string> <string name="pref_about_title">Om</string>
<string name="track_list_p_element_statistics">Samlet registreret distance</string> <string name="track_list_p_element_statistics">Samlet registreret distance</string>
<string name="pref_report_issue_summary">Rapporter fejl og foreslå forbedringer på GitHub.</string> <string name="pref_report_issue_summary">Rapporter fejl og foreslå forbedringer på GitHub.</string>
<string name="pref_recording_accuracy_title">Nøjagtighed af optagelser</string>
<string name="pref_recording_accuracy_summary_default">Waypoints har lavere nøjagtighed, men er mere hyppige.</string>
<string name="pref_recording_accuracy_summary_high">Waypoints har større nøjagtighed, men er mindre hyppige.</string>
<string name="pref_imperial_measurement_units_summary_metric">I øjeblikket anvendes metriske enheder (kilometer, meter).</string> <string name="pref_imperial_measurement_units_summary_metric">I øjeblikket anvendes metriske enheder (kilometer, meter).</string>
<string name="pref_gps_only_summary_gps_only">I øjeblikket bruger vi kun GPS til lokalisering.</string> <string name="pref_gps_only_summary_gps_only">I øjeblikket bruger vi kun GPS til lokalisering.</string>
<string name="pref_gps_only_summary_gps_and_network">I øjeblikket bruges GPS og netværk til lokalisering.</string> <string name="pref_gps_only_summary_gps_and_network">I øjeblikket bruges GPS og netværk til lokalisering.</string>

View file

@ -18,7 +18,6 @@
<!-- fab sub menu_bottom_navigation --> <!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Zurücksetzen</string> <string name="button_delete">Zurücksetzen</string>
<string name="button_save">Speichern</string> <string name="button_save">Speichern</string>
<string name="button_resume">Fortsetzen</string>
<!-- dialogs --> <!-- dialogs -->
<string name="dialog_share_gpx">GPX-Datei teilen mit</string> <string name="dialog_share_gpx">GPX-Datei teilen mit</string>
<string name="dialog_error_empty_recording_message">Trackbook hat noch keine Wegpunkte aufgenommen.</string> <string name="dialog_error_empty_recording_message">Trackbook hat noch keine Wegpunkte aufgenommen.</string>
@ -110,7 +109,4 @@
<string name="toast_message_poi_removed">Markierung für Ort von Interesse entfernt.</string> <string name="toast_message_poi_removed">Markierung für Ort von Interesse entfernt.</string>
<string name="toast_message_poi_added">Markierung für Ort von Interesse hinzugefügt.</string> <string name="toast_message_poi_added">Markierung für Ort von Interesse hinzugefügt.</string>
<string name="track_list_p_element_statistics">Erfasste Gesamtentfernung</string> <string name="track_list_p_element_statistics">Erfasste Gesamtentfernung</string>
<string name="pref_recording_accuracy_summary_high">Wegpunkte haben eine höhere Genauigkeit, sind aber weniger häufig.</string>
<string name="pref_recording_accuracy_summary_default">Wegpunkte haben eine geringere Genauigkeit, sind aber häufiger.</string>
<string name="pref_recording_accuracy_title">Aufzeichnungsgenauigkeit</string>
</resources> </resources>

View file

@ -28,7 +28,6 @@
<string name="snackbar_message_location_permission_denied">Permiso de ubicación no concedido. Trackbook no funcionará.</string> <string name="snackbar_message_location_permission_denied">Permiso de ubicación no concedido. Trackbook no funcionará.</string>
<string name="button_delete">Limpiar</string> <string name="button_delete">Limpiar</string>
<string name="button_save">Guardar</string> <string name="button_save">Guardar</string>
<string name="button_resume">Resumir</string>
<string name="dialog_generic_button_okay">OK</string> <string name="dialog_generic_button_okay">OK</string>
<string name="dialog_generic_details_button">Mostrar detalles</string> <string name="dialog_generic_details_button">Mostrar detalles</string>
<string name="dialog_error_empty_recording_message">Trackbook no registró ningún punto de referencia hasta el momento.</string> <string name="dialog_error_empty_recording_message">Trackbook no registró ningún punto de referencia hasta el momento.</string>
@ -77,9 +76,6 @@
<string name="pref_gps_only_summary_gps_and_network">Actualmente usando GPS y Red para localización.</string> <string name="pref_gps_only_summary_gps_and_network">Actualmente usando GPS y Red para localización.</string>
<string name="pref_imperial_measurement_units_summary_metric">Actualmente se utilizan unidades métricas (Kilómetro, Metro).</string> <string name="pref_imperial_measurement_units_summary_metric">Actualmente se utilizan unidades métricas (Kilómetro, Metro).</string>
<string name="pref_imperial_measurement_units_title">Utilizar medidas imperiales</string> <string name="pref_imperial_measurement_units_title">Utilizar medidas imperiales</string>
<string name="pref_recording_accuracy_summary_high">Los puntos de referencia tienen mayor precisión pero son menos frecuentes.</string>
<string name="pref_recording_accuracy_summary_default">Los waypoints tienen menor precisión pero son más frecuentes.</string>
<string name="pref_recording_accuracy_title">Precisión de grabación</string>
<string name="pref_report_issue_title">Reportar problema</string> <string name="pref_report_issue_title">Reportar problema</string>
<string name="pref_reset_advanced_summary">Restablece la configuración avanzada a los valores predeterminados.</string> <string name="pref_reset_advanced_summary">Restablece la configuración avanzada a los valores predeterminados.</string>
<string name="pref_reset_advanced_title">Reiniciar</string> <string name="pref_reset_advanced_title">Reiniciar</string>

View file

@ -17,7 +17,6 @@
<string name="snackbar_message_location_offline">Localisation désactivée. Le suivi ne fonctionnera pas.</string> <string name="snackbar_message_location_offline">Localisation désactivée. Le suivi ne fonctionnera pas.</string>
<!-- fab sub menu_bottom_navigation --> <!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Supprimer</string> <string name="button_delete">Supprimer</string>
<string name="button_resume">Reprendre</string>
<string name="button_save">Sauvegarder</string> <string name="button_save">Sauvegarder</string>
<!-- dialogs --> <!-- dialogs -->
<string name="dialog_error_empty_recording_button_resume">Reprendre l\'enregistrement</string> <string name="dialog_error_empty_recording_button_resume">Reprendre l\'enregistrement</string>
@ -109,9 +108,6 @@
<string name="toast_message_copied_to_clipboard">Copié dans le presse-papiers.</string> <string name="toast_message_copied_to_clipboard">Copié dans le presse-papiers.</string>
<string name="toast_message_poi_removed">Marqueur de point d\'intérêt supprimé.</string> <string name="toast_message_poi_removed">Marqueur de point d\'intérêt supprimé.</string>
<string name="toast_message_poi_added">Marqueur de point d\'intérêt ajouté.</string> <string name="toast_message_poi_added">Marqueur de point d\'intérêt ajouté.</string>
<string name="pref_recording_accuracy_title">Précision de l\'enregistrement</string>
<string name="pref_recording_accuracy_summary_default">Les points de cheminements ont une précision plus faible mais sont plus fréquents.</string>
<string name="pref_recording_accuracy_summary_high">Les points de cheminement ont une plus grande précision mais sont moins fréquents.</string>
<string name="track_list_p_element_statistics">Distance totale enregistrée</string> <string name="track_list_p_element_statistics">Distance totale enregistrée</string>
<string name="descr_button_location">Centrer sur la position actuelle</string> <string name="descr_button_location">Centrer sur la position actuelle</string>
</resources> </resources>

View file

@ -32,7 +32,6 @@
<string name="button_save">Spremi</string> <string name="button_save">Spremi</string>
<string name="pref_general_title">Opće</string> <string name="pref_general_title">Opće</string>
<string name="pref_accuracy_threshold_title">Prag točnosti</string> <string name="pref_accuracy_threshold_title">Prag točnosti</string>
<string name="button_resume">Nastavi</string>
<string name="pref_theme_selection_title">Tema programa</string> <string name="pref_theme_selection_title">Tema programa</string>
<string name="tab_tracks">Rute</string> <string name="tab_tracks">Rute</string>
<string name="descr_map_last_track">Mapa zadnje rute</string> <string name="descr_map_last_track">Mapa zadnje rute</string>
@ -96,9 +95,6 @@
<string name="toast_message_copied_to_clipboard">Kopirano u međuspremnik.</string> <string name="toast_message_copied_to_clipboard">Kopirano u međuspremnik.</string>
<string name="toast_message_poi_removed">Oznaka točke interesa uklonjena.</string> <string name="toast_message_poi_removed">Oznaka točke interesa uklonjena.</string>
<string name="toast_message_poi_added">Oznaka točke interesa dodana.</string> <string name="toast_message_poi_added">Oznaka točke interesa dodana.</string>
<string name="pref_recording_accuracy_summary_high">Točke rute imaju veću točnost ali su rjeđe.</string>
<string name="pref_recording_accuracy_title">Točnost snimanja</string>
<string name="pref_recording_accuracy_summary_default">Točke rute imaju manju točnost ali su češće.</string>
<string name="track_list_p_element_statistics">Ukupna udaljenost snimljena</string> <string name="track_list_p_element_statistics">Ukupna udaljenost snimljena</string>
<string name="descr_button_location">Centriraj na trenutačno mjesto</string> <string name="descr_button_location">Centriraj na trenutačno mjesto</string>
</resources> </resources>

View file

@ -182,7 +182,6 @@
<string name="dialog_generic_details_button">Tampilkan detil</string> <string name="dialog_generic_details_button">Tampilkan detil</string>
<string name="dialog_generic_button_okay">Oke</string> <string name="dialog_generic_button_okay">Oke</string>
<string name="dialog_generic_button_cancel">Batalkan</string> <string name="dialog_generic_button_cancel">Batalkan</string>
<string name="button_resume">Lanjutkan</string>
<string name="button_save">Simpan</string> <string name="button_save">Simpan</string>
<string name="button_delete">Bersihkan</string> <string name="button_delete">Bersihkan</string>
<string name="snackbar_message_location_permission_denied">Izin lokasi tak diberikan. Trackbook tidak akan berfungsi.</string> <string name="snackbar_message_location_permission_denied">Izin lokasi tak diberikan. Trackbook tidak akan berfungsi.</string>
@ -192,8 +191,5 @@
<string name="notification_show">Tampilkan</string> <string name="notification_show">Tampilkan</string>
<string name="notification_resume">Lanjutkan</string> <string name="notification_resume">Lanjutkan</string>
<string name="tab_settings">Pengaturan</string> <string name="tab_settings">Pengaturan</string>
<string name="pref_recording_accuracy_summary_default">Waypoints memiliki akurasi yang lebih rendah tetapi lebih sering.</string>
<string name="pref_recording_accuracy_summary_high">Waypoints memiliki akurasi yang lebih tinggi tetapi lebih jarang.</string>
<string name="pref_recording_accuracy_title">Akurasi Perekaman</string>
<string name="track_list_p_element_statistics">Jarak Total Terekam</string> <string name="track_list_p_element_statistics">Jarak Total Terekam</string>
</resources> </resources>

View file

@ -21,7 +21,6 @@
<!-- FAB Sub Menu --> <!-- FAB Sub Menu -->
<string name="button_delete">Cancella</string> <string name="button_delete">Cancella</string>
<string name="button_save">Salva</string> <string name="button_save">Salva</string>
<string name="button_resume">Riprendi</string>
<!-- Dialogs --> <!-- Dialogs -->
<string name="dialog_generic_button_cancel">Annulla</string> <string name="dialog_generic_button_cancel">Annulla</string>
<string name="dialog_generic_button_okay">OK</string> <string name="dialog_generic_button_okay">OK</string>
@ -116,8 +115,5 @@
<string name="sample_text_track_data" translatable="false">23.0 km • 5 hrs 23 min 42 sec</string> <string name="sample_text_track_data" translatable="false">23.0 km • 5 hrs 23 min 42 sec</string>
<string name="sample_text_track_name" translatable="false">July 20, 1969</string> <string name="sample_text_track_name" translatable="false">July 20, 1969</string>
<string name="sample_text_default_data" translatable="false">track data missing</string> <string name="sample_text_default_data" translatable="false">track data missing</string>
<string name="pref_recording_accuracy_title">Precisione di registrazione</string>
<string name="pref_recording_accuracy_summary_high">I waypoint hanno una maggiore precisione ma sono meno frequenti.</string>
<string name="pref_recording_accuracy_summary_default">I waypoint hanno una precisione inferiore ma sono più frequenti.</string>
<string name="track_list_p_element_statistics">Distanza totale registrata</string> <string name="track_list_p_element_statistics">Distanza totale registrata</string>
</resources> </resources>

View file

@ -18,7 +18,6 @@
<!-- fab sub menu_bottom_navigation --> <!-- fab sub menu_bottom_navigation -->
<string name="button_delete">クリア</string> <string name="button_delete">クリア</string>
<string name="button_save">保存してクリア</string> <string name="button_save">保存してクリア</string>
<string name="button_resume">再開</string>
<!-- dialogs --> <!-- dialogs -->
<string name="dialog_share_gpx">GPX ファイルを共有...</string> <string name="dialog_share_gpx">GPX ファイルを共有...</string>
<string name="dialog_error_empty_recording_message">トラックブックはこれまでウェイポイントを記録していません。</string> <string name="dialog_error_empty_recording_message">トラックブックはこれまでウェイポイントを記録していません。</string>

View file

@ -18,7 +18,6 @@
<!-- fab sub menu_bottom_navigation --> <!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Tøm</string> <string name="button_delete">Tøm</string>
<string name="button_save">Lagre og tøm</string> <string name="button_save">Lagre og tøm</string>
<string name="button_resume">Fortsett</string>
<!-- dialogs --> <!-- dialogs -->
<string name="dialog_share_gpx">Del GPX-fil med</string> <string name="dialog_share_gpx">Del GPX-fil med</string>
<string name="dialog_error_empty_recording_message">Trackbook har ikke registrert noen veipunkter så langt.</string> <string name="dialog_error_empty_recording_message">Trackbook har ikke registrert noen veipunkter så langt.</string>
@ -109,9 +108,6 @@
<string name="toast_message_poi_added">Interessepunktmarkør lagt til.</string> <string name="toast_message_poi_added">Interessepunktmarkør lagt til.</string>
<string name="toast_message_poi_removed">Interessepunktmarkør fjernet.</string> <string name="toast_message_poi_removed">Interessepunktmarkør fjernet.</string>
<string name="toast_message_copied_to_clipboard">Kopiert til utklippstavle.</string> <string name="toast_message_copied_to_clipboard">Kopiert til utklippstavle.</string>
<string name="pref_recording_accuracy_summary_high">Veipunkter har høyere nøyaktighet, men er mindre hyppige.</string>
<string name="track_list_p_element_statistics">Totalavstand registrert</string> <string name="track_list_p_element_statistics">Totalavstand registrert</string>
<string name="pref_recording_accuracy_summary_default">Veipunkter har lavere nøyaktighet, men er hyppigere.</string>
<string name="pref_recording_accuracy_title">Opptaksnøyaktighet</string>
<string name="descr_button_location">Sentrer på nåværende sted</string> <string name="descr_button_location">Sentrer på nåværende sted</string>
</resources> </resources>

View file

@ -18,7 +18,6 @@
<!-- fab sub menu_bottom_navigation --> <!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Wissen</string> <string name="button_delete">Wissen</string>
<string name="button_save">Opslaan</string> <string name="button_save">Opslaan</string>
<string name="button_resume">Hervatten</string>
<!-- dialogs --> <!-- dialogs -->
<string name="dialog_share_gpx">GPX-bestand delen met</string> <string name="dialog_share_gpx">GPX-bestand delen met</string>
<string name="dialog_error_empty_recording_message">Trackbook heeft nog geen routepunten vastgelegd.</string> <string name="dialog_error_empty_recording_message">Trackbook heeft nog geen routepunten vastgelegd.</string>
@ -109,9 +108,6 @@
<string name="toast_message_copied_to_clipboard">Gekopieerd naar klembord.</string> <string name="toast_message_copied_to_clipboard">Gekopieerd naar klembord.</string>
<string name="toast_message_poi_removed">POI-indicatie verwijderd.</string> <string name="toast_message_poi_removed">POI-indicatie verwijderd.</string>
<string name="toast_message_poi_added">POI-indicatie toegevoegd.</string> <string name="toast_message_poi_added">POI-indicatie toegevoegd.</string>
<string name="pref_recording_accuracy_title">Opname nauwkeurigheid</string>
<string name="pref_recording_accuracy_summary_default">Routepunten zijn minder nauwkeurig, maar frequenter.</string>
<string name="pref_recording_accuracy_summary_high">Routepunten zijn nauwkeuriger, maar minder frequent.</string>
<string name="track_list_p_element_statistics">Totaal afgelegde afstand</string> <string name="track_list_p_element_statistics">Totaal afgelegde afstand</string>
<string name="descr_button_location">Centreren op huidige locatie</string> <string name="descr_button_location">Centreren op huidige locatie</string>
</resources> </resources>

View file

@ -3,7 +3,6 @@
<string name="dialog_generic_details_button">Pokaż szczegóły</string> <string name="dialog_generic_details_button">Pokaż szczegóły</string>
<string name="dialog_generic_button_okay">OK</string> <string name="dialog_generic_button_okay">OK</string>
<string name="dialog_generic_button_cancel">Anuluj</string> <string name="dialog_generic_button_cancel">Anuluj</string>
<string name="button_resume">Kontynuuj</string>
<string name="button_save">Zapisz</string> <string name="button_save">Zapisz</string>
<string name="button_delete">Wyczyść</string> <string name="button_delete">Wyczyść</string>
<string name="snackbar_message_location_offline">Lokalizacja jest wyłączona. Trackbook nie będzie działać.</string> <string name="snackbar_message_location_offline">Lokalizacja jest wyłączona. Trackbook nie będzie działać.</string>
@ -76,9 +75,6 @@
<string name="pref_reset_advanced_title">Resetuj</string> <string name="pref_reset_advanced_title">Resetuj</string>
<string name="pref_reset_advanced_summary">Przywróć ustawienia zaawansowane do wartości domyślnych.</string> <string name="pref_reset_advanced_summary">Przywróć ustawienia zaawansowane do wartości domyślnych.</string>
<string name="pref_report_issue_summary">Zgłaszaj błędy i proponuj ulepszenia na GitHub.</string> <string name="pref_report_issue_summary">Zgłaszaj błędy i proponuj ulepszenia na GitHub.</string>
<string name="pref_recording_accuracy_title">Dokładność zapisu</string>
<string name="pref_recording_accuracy_summary_default">Punkty trasy mają mniejszą dokładność, ale są częstsze.</string>
<string name="pref_recording_accuracy_summary_high">Punkty trasy mają większą dokładność, ale występują rzadziej.</string>
<string name="descr_button_resume">Przycisk Wznów</string> <string name="descr_button_resume">Przycisk Wznów</string>
<string name="descr_map_current_track">Odwzorowanie bieżącego toru</string> <string name="descr_map_current_track">Odwzorowanie bieżącego toru</string>
<string name="abbreviation_minutes">min</string> <string name="abbreviation_minutes">min</string>

View file

@ -3,7 +3,6 @@
<!-- App Name --> <!-- App Name -->
<string name="app_name">Trackbook</string> <string name="app_name">Trackbook</string>
<!-- please do not translate app_name - transcription into different alphabet types is fine though --> <!-- please do not translate app_name - transcription into different alphabet types is fine though -->
<string name="app_version_name" translatable="false">\"Echoes\"</string>
<!-- Tabs --> <!-- Tabs -->
<string name="tab_map">Mapa</string> <string name="tab_map">Mapa</string>
<string name="tab_tracks">Caminhos</string> <string name="tab_tracks">Caminhos</string>
@ -22,7 +21,6 @@
<!-- FAB Sub Menu --> <!-- FAB Sub Menu -->
<string name="button_delete">Limpar</string> <string name="button_delete">Limpar</string>
<string name="button_save">Salvar</string> <string name="button_save">Salvar</string>
<string name="button_resume">Retomar</string>
<!-- Dialogs --> <!-- Dialogs -->
<string name="dialog_generic_button_cancel">Cancelar</string> <string name="dialog_generic_button_cancel">Cancelar</string>
<string name="dialog_generic_button_okay">OK</string> <string name="dialog_generic_button_okay">OK</string>
@ -113,8 +111,5 @@
<string name="descr_statistics_sheet_delete_button">Botão apagar rota</string> <string name="descr_statistics_sheet_delete_button">Botão apagar rota</string>
<string name="descr_statistics_sheet_edit_button">Botão editar rota</string> <string name="descr_statistics_sheet_edit_button">Botão editar rota</string>
<string name="descr_statistics_sheet_save_button">Botão salvar como GPX</string> <string name="descr_statistics_sheet_save_button">Botão salvar como GPX</string>
<string name="pref_recording_accuracy_summary_high">Os waypoints têm maior precisão, mas são menos freqüentes.</string>
<string name="pref_recording_accuracy_title">Precisão de Gravação</string>
<string name="track_list_p_element_statistics">Distância Total Registrada</string> <string name="track_list_p_element_statistics">Distância Total Registrada</string>
<string name="pref_recording_accuracy_summary_default">Os waypoints têm menor precisão, mas são mais freqüentes.</string>
</resources> </resources>

View file

@ -33,7 +33,6 @@
<string name="dialog_generic_details_button">Показать подробности</string> <string name="dialog_generic_details_button">Показать подробности</string>
<string name="dialog_generic_button_okay">ОЕ</string> <string name="dialog_generic_button_okay">ОЕ</string>
<string name="dialog_generic_button_cancel">Отмена</string> <string name="dialog_generic_button_cancel">Отмена</string>
<string name="button_resume">Продолжить</string>
<string name="button_save">Сохранить</string> <string name="button_save">Сохранить</string>
<string name="button_delete">Очистить</string> <string name="button_delete">Очистить</string>
<string name="snackbar_message_location_permission_denied">Разрешение на определение местоположения не предоставлено. Trackbook работать не будет.</string> <string name="snackbar_message_location_permission_denied">Разрешение на определение местоположения не предоставлено. Trackbook работать не будет.</string>
@ -67,8 +66,6 @@
<string name="pref_gps_only_summary_gps_and_network">В настоящее время для локализации используется GPS и сеть.</string> <string name="pref_gps_only_summary_gps_and_network">В настоящее время для локализации используется GPS и сеть.</string>
<string name="pref_imperial_measurement_units_summary_metric">В настоящее время используются метрические единицы (километр, метр).</string> <string name="pref_imperial_measurement_units_summary_metric">В настоящее время используются метрические единицы (километр, метр).</string>
<string name="pref_gps_only_summary_gps_only">В настоящее время для локализации используется только GPS.</string> <string name="pref_gps_only_summary_gps_only">В настоящее время для локализации используется только GPS.</string>
<string name="pref_recording_accuracy_title">Точность записи</string>
<string name="pref_recording_accuracy_summary_default">Путевые точки имеют более низкую точность, но встречаются чаще.</string>
<string name="pref_reset_advanced_summary">Сброс расширенных настроек до значений по умолчанию.</string> <string name="pref_reset_advanced_summary">Сброс расширенных настроек до значений по умолчанию.</string>
<string name="pref_report_issue_title">Выпуск отчета</string> <string name="pref_report_issue_title">Выпуск отчета</string>
<string name="pref_theme_selection_mode_dark">Темный режим</string> <string name="pref_theme_selection_mode_dark">Темный режим</string>
@ -81,7 +78,6 @@
<string name="abbreviation_seconds">сек</string> <string name="abbreviation_seconds">сек</string>
<string name="abbreviation_minutes">мин</string> <string name="abbreviation_minutes">мин</string>
<string name="pref_report_issue_summary">Сообщайте об ошибках и предлагайте улучшения на GitHub.</string> <string name="pref_report_issue_summary">Сообщайте об ошибках и предлагайте улучшения на GitHub.</string>
<string name="pref_recording_accuracy_summary_high">Путевые точки имеют более высокую точность, но встречаются реже.</string>
<string name="pref_imperial_measurement_units_title">Используйте имперские меры</string> <string name="pref_imperial_measurement_units_title">Используйте имперские меры</string>
<string name="pref_imperial_measurement_units_summary_imperial">В настоящее время используются имперские единицы (мили, футы).</string> <string name="pref_imperial_measurement_units_summary_imperial">В настоящее время используются имперские единицы (мили, футы).</string>
<string name="pref_gps_only_title">Ограничение на GPS</string> <string name="pref_gps_only_title">Ограничение на GPS</string>

View file

@ -19,7 +19,6 @@
<!-- fab sub menu_bottom_navigation --> <!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Rensa</string> <string name="button_delete">Rensa</string>
<string name="button_save">Spara</string> <string name="button_save">Spara</string>
<string name="button_resume">Återuppta</string>
<!-- dialogs --> <!-- dialogs -->
<string name="dialog_share_gpx">Dela GPX-fil med</string> <string name="dialog_share_gpx">Dela GPX-fil med</string>
<string name="dialog_error_empty_recording_message">Trackbook spelade inte in några vägpunkter så här långt.</string> <string name="dialog_error_empty_recording_message">Trackbook spelade inte in några vägpunkter så här långt.</string>
@ -96,7 +95,6 @@
<string name="descr_statistics_sheet_edit_button">Spårredigeringsknapp</string> <string name="descr_statistics_sheet_edit_button">Spårredigeringsknapp</string>
<string name="pref_reset_advanced_title">Återställ</string> <string name="pref_reset_advanced_title">Återställ</string>
<string name="toast_message_elevation_info">Tips: Noggrannheten hos höjddata beror på din enhet. Höjden i upp- och nedförsbacke för hela rutten mäts.</string> <string name="toast_message_elevation_info">Tips: Noggrannheten hos höjddata beror på din enhet. Höjden i upp- och nedförsbacke för hela rutten mäts.</string>
<string name="pref_recording_accuracy_summary_high">Vägpunkter har högre noggrannhet men är mindre frekventa.</string>
<string name="pref_imperial_measurement_units_summary_metric">För närvarande används metriska enheter (kilometer, meter).</string> <string name="pref_imperial_measurement_units_summary_metric">För närvarande används metriska enheter (kilometer, meter).</string>
<string name="pref_advanced_title">Avancerad</string> <string name="pref_advanced_title">Avancerad</string>
<string name="pref_accuracy_threshold_title">Tröskel för noggrannhet</string> <string name="pref_accuracy_threshold_title">Tröskel för noggrannhet</string>
@ -115,7 +113,6 @@
<string name="pref_reset_advanced_summary">Återställ avancerade inställningar till standardinställningarna.</string> <string name="pref_reset_advanced_summary">Återställ avancerade inställningar till standardinställningarna.</string>
<string name="pref_report_issue_title">Rapportera frågan</string> <string name="pref_report_issue_title">Rapportera frågan</string>
<string name="pref_report_issue_summary">Rapportera fel och föreslå förbättringar på GitHub.</string> <string name="pref_report_issue_summary">Rapportera fel och föreslå förbättringar på GitHub.</string>
<string name="pref_recording_accuracy_title">Noggrannhet vid inspelning</string>
<string name="track_list_onboarding_h1_part_2">... kommer att visas här.</string> <string name="track_list_onboarding_h1_part_2">... kommer att visas här.</string>
<string name="track_list_onboarding_h1_part_1">Dina inspelade spår</string> <string name="track_list_onboarding_h1_part_1">Dina inspelade spår</string>
<string name="layout_onboarding_description_app_icon">Ikon för Trackbook-appen</string> <string name="layout_onboarding_description_app_icon">Ikon för Trackbook-appen</string>
@ -156,7 +153,6 @@
<string name="descr_quick_settings_tile_title_pause">Stoppa inspelningen</string> <string name="descr_quick_settings_tile_title_pause">Stoppa inspelningen</string>
<string name="descr_mark_starred_button">Markera som stjärnmärkt</string> <string name="descr_mark_starred_button">Markera som stjärnmärkt</string>
<string name="descr_button_resume">Återuppta inspelning</string> <string name="descr_button_resume">Återuppta inspelning</string>
<string name="pref_recording_accuracy_summary_default">Vägpunkter har lägre noggrannhet men är mer frekventa.</string>
<string name="statistics_sheet_p_duration">Total varaktighet:</string> <string name="statistics_sheet_p_duration">Total varaktighet:</string>
<string name="pref_gps_only_summary_gps_only">För närvarande används endast GPS för lokalisering.</string> <string name="pref_gps_only_summary_gps_only">För närvarande används endast GPS för lokalisering.</string>
<string name="pref_delete_non_starred_summary">Ta bort alla inspelningar i \"Tracks\" som inte har stjärnor.</string> <string name="pref_delete_non_starred_summary">Ta bort alla inspelningar i \"Tracks\" som inte har stjärnor.</string>

View file

@ -8,7 +8,6 @@
<string name="descr_mark_starred_button">Yıldızlı olarak işaretle düğmesi</string> <string name="descr_mark_starred_button">Yıldızlı olarak işaretle düğmesi</string>
<string name="descr_button_resume">Kaydı devam ettir</string> <string name="descr_button_resume">Kaydı devam ettir</string>
<string name="notification_resume">Devam ettir</string> <string name="notification_resume">Devam ettir</string>
<string name="button_resume">Devam ettir</string>
<string name="descr_button_delete">Kaydı temizle</string> <string name="descr_button_delete">Kaydı temizle</string>
<string name="descr_button_save">Kaydı kaydet</string> <string name="descr_button_save">Kaydı kaydet</string>
<string name="descr_button_start">Kaydı başlat düğmesi</string> <string name="descr_button_start">Kaydı başlat düğmesi</string>
@ -96,9 +95,6 @@
<string name="tab_tracks">Yollar</string> <string name="tab_tracks">Yollar</string>
<string name="tab_map">Harita</string> <string name="tab_map">Harita</string>
<string name="app_name">Trackbook</string> <string name="app_name">Trackbook</string>
<string name="pref_recording_accuracy_title">Kayıt Doğruluğu</string>
<string name="pref_recording_accuracy_summary_default">Ara noktalar daha düşük doğruluğa sahiptir ancak daha sıktır.</string>
<string name="pref_recording_accuracy_summary_high">Ara noktalar daha yüksek doğruluğa sahiptir ancak daha az sıklıktadır.</string>
<string name="track_list_p_element_statistics">Kaydedilen Toplam Mesafe</string> <string name="track_list_p_element_statistics">Kaydedilen Toplam Mesafe</string>
<string name="descr_button_location">Geçerli konuma ortala</string> <string name="descr_button_location">Geçerli konuma ortala</string>
</resources> </resources>

View file

@ -21,7 +21,6 @@
<!-- Buttons --> <!-- Buttons -->
<string name="button_delete">删除</string> <string name="button_delete">删除</string>
<string name="button_pause">暂停</string> <string name="button_pause">暂停</string>
<string name="button_resume">恢复</string>
<string name="button_save">保存</string> <string name="button_save">保存</string>
<string name="button_start">开始</string> <string name="button_start">开始</string>
<!-- Dialogs --> <!-- Dialogs -->
@ -92,9 +91,6 @@
<string name="pref_imperial_measurement_units_summary_metric">当前正使用公制单位(千米,米)。</string> <string name="pref_imperial_measurement_units_summary_metric">当前正使用公制单位(千米,米)。</string>
<string name="pref_imperial_measurement_units_summary_imperial">当前正使用英制单位(英里,英尺)。</string> <string name="pref_imperial_measurement_units_summary_imperial">当前正使用英制单位(英里,英尺)。</string>
<string name="pref_imperial_measurement_units_title">使用英制测量</string> <string name="pref_imperial_measurement_units_title">使用英制测量</string>
<string name="pref_recording_accuracy_summary_high">航点的精确度较高,但频率较低。</string>
<string name="pref_recording_accuracy_summary_default">航点的精确度较低,但频率较高。</string>
<string name="pref_recording_accuracy_title">记录精确度</string>
<string name="pref_report_issue_summary">在 Github 上报告漏洞并提出改进建议。</string> <string name="pref_report_issue_summary">在 Github 上报告漏洞并提出改进建议。</string>
<string name="pref_report_issue_title">报告问题</string> <string name="pref_report_issue_title">报告问题</string>
<string name="pref_reset_advanced_summary">重置高级设置为默认值。</string> <string name="pref_reset_advanced_summary">重置高级设置为默认值。</string>

View file

@ -47,6 +47,7 @@
<color name="default_red">#FFDC3D33</color> <!-- Slightly muted variant of -> Material Design 2: Red 600 --> <color name="default_red">#FFDC3D33</color> <!-- Slightly muted variant of -> Material Design 2: Red 600 -->
<color name="default_red_dark">#FFCA2D23</color> <color name="default_red_dark">#FFCA2D23</color>
<color name="default_blue">#FF3C98DB</color> <color name="default_blue">#FF3C98DB</color>
<color name="homepoint">#FFFFC107</color>
<color name="default_green">#FF4CAF50</color> <color name="default_green">#FF4CAF50</color>
</resources> </resources>

View file

@ -1,18 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- App Name --> <!-- App Name -->
<string name="app_name">Trackbook-v</string> <string name="app_name">trkpt</string>
<!-- please do not translate app_name - transcription into different alphabet types is fine though --> <!-- please do not translate app_name - transcription into different alphabet types is fine though -->
<string name="app_version_name" translatable="false">\"See Emily Play\"</string>
<!-- Tabs --> <!-- Tabs -->
<string name="tab_map">Map</string> <string name="tab_map">Map</string>
<string name="tab_tracks">Tracks</string> <string name="tab_tracks">Tracks</string>
<string name="tab_settings">Settings</string> <string name="tab_settings">Settings</string>
<!-- Notification --> <!-- Notification -->
<string name="notification_title_trackbook_running">Trackbook running</string> <string name="notification_title_trackbook_running">Trackbook running</string>
<string name="notification_title_trackbook_not_running">Trackbook not running</string> <string name="notification_title_trackbook_not_running">Trackbook stopped</string>
<string name="notification_pause">Pause</string> <string name="notification_pause">Stop</string>
<string name="notification_resume">Resume</string> <string name="notification_resume">Record</string>
<string name="notification_show">Show</string> <string name="notification_show">Show</string>
<string name="notification_channel_recording_name">Movement Recording State</string> <string name="notification_channel_recording_name">Movement Recording State</string>
<string name="notification_channel_recording_description">Display duration and distance. Option to pause movement recording.</string> <string name="notification_channel_recording_description">Display duration and distance. Option to pause movement recording.</string>
@ -21,10 +20,9 @@
<string name="snackbar_message_location_permission_denied">Location permission not granted. Trackbook will not work.</string> <string name="snackbar_message_location_permission_denied">Location permission not granted. Trackbook will not work.</string>
<!-- Buttons --> <!-- Buttons -->
<string name="button_delete">Delete</string> <string name="button_delete">Delete</string>
<string name="button_pause">Pause</string> <string name="button_pause">Stop</string>
<string name="button_resume">Resume</string>
<string name="button_save">Save</string> <string name="button_save">Save</string>
<string name="button_start">Start</string> <string name="button_start">Record</string>
<!-- Dialogs --> <!-- Dialogs -->
<string name="dialog_delete_current_recording_message">Discard current recording?</string> <string name="dialog_delete_current_recording_message">Discard current recording?</string>
<string name="dialog_delete_current_recording_button_discard">Discard</string> <string name="dialog_delete_current_recording_button_discard">Discard</string>
@ -86,7 +84,9 @@
<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_summary">Automatically export GPX file after this many hours.</string>
<string name="pref_device_id_summary">A unique ID to distinguish tracks recorded across multiple devices.</string>
<string name="pref_auto_export_interval_title">Auto Export Interval</string> <string name="pref_auto_export_interval_title">Auto Export Interval</string>
<string name="pref_device_id">Device ID</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>
@ -117,7 +117,7 @@
<!-- Descriptions --> <!-- Descriptions -->
<string name="descr_button_delete">Discard recording</string> <string name="descr_button_delete">Discard recording</string>
<string name="descr_button_location">Center on current location</string> <string name="descr_button_location">Center on current location</string>
<string name="descr_button_pause">Pause recording</string> <string name="descr_button_pause">Stop recording</string>
<string name="descr_button_resume">Resume recording</string> <string name="descr_button_resume">Resume recording</string>
<string name="descr_button_save">Save recording</string> <string name="descr_button_save">Save recording</string>
<string name="descr_button_start">Start recording</string> <string name="descr_button_start">Start recording</string>