checkpoint

master
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'
android {
compileSdkVersion 31
compileSdkVersion 33
defaultConfig {
applicationId 'net.voussoir.trackbook'
applicationId 'net.voussoir.trkpt'
minSdkVersion 25
targetSdkVersion 31
targetSdk 32
versionCode 50
versionName '2.1.2'
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 {
// Kotlin
def coroutinesVersion = "1.5.2"
def coroutinesVersion = '1.6.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
// AndroidX
def navigationVersion = "2.3.5"
implementation "androidx.activity:activity-ktx:1.4.0"
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.core:core-ktx:1.7.0'
def navigationVersion = '2.5.3'
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.9.0'
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 'com.google.android.material:material:1.6.0-alpha03'
implementation 'com.google.android.material:material:1.9.0-alpha02'
// Gson
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.google.code.gson:gson:2.10.1'
// 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.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".Trackbook"
android:name="org.y20k.trackbook.Trackbook"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"

View File

@ -26,7 +26,7 @@ import java.util.*
object Keys {
// application name
const val APPLICATION_NAME: String = "Trackbook"
const val APPLICATION_NAME: String = "trkpt"
// version numbers
const val CURRENT_TRACK_FORMAT_VERSION: Int = 4
@ -39,9 +39,10 @@ object Keys {
// args
const val ARG_TRACK_TITLE: String = "ArgTrackTitle"
const val ARG_TRACK_FILE_URI: String = "ArgTrackFileUri"
const val ARG_GPX_FILE_URI: String = "ArgGpxFileUri"
const val ARG_TRACK_ID: String = "ArgTrackId"
const val ARG_TRACK_ID: String = "ArgTrackID"
const val ARG_TRACK_DEVICE_ID: String = "ArgTrackDeviceID"
const val ARG_TRACK_START_TIME: String = "ArgTrackStartTime"
const val ARG_TRACK_STOP_TIME: String = "ArgTrackStopTime"
// 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
@ -57,15 +58,12 @@ object Keys {
const val PREF_USE_IMPERIAL_UNITS: String = "prefUseImperialUnits"
const val PREF_GPS_ONLY: String = "prefGpsOnly"
const val PREF_OMIT_RESTS: String = "prefOmitRests"
const val PREF_AUTO_EXPORT_INTERVAL: String = "prefAutoExportInterval"
const val PREF_ALTITUDE_SMOOTHING_VALUE: String = "prefAltitudeSmoothingValue"
const val PREF_LOCATION_ACCURACY_THRESHOLD: String = "prefLocationAccuracyThreshold"
const val PREF_LOCATION_AGE_THRESHOLD: String = "prefLocationAgeThreshold"
const val PREF_COMMIT_INTERVAL: String = "prefCommitInterval"
const val PREF_DEVICE_ID: String = "prefDeviceID"
// 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_PAUSED: Int = 2
const val STATE_THEME_FOLLOW_SYSTEM: String = "stateFollowSystem"
const val STATE_THEME_LIGHT_MODE: String = "stateLightMode"
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 EMPTY_STRING_RESOURCE: Int = 0
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 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 IMPLAUSIBLE_TRACK_START_SPEED: Double = 250.0 // 250 km/h
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_ALTITUDE: Double = 0.0
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_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_DISTANCE: Float = 15f // 15 meters
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
// notification

View File

@ -17,7 +17,6 @@
package org.y20k.trackbook
import YesNoDialog
import android.Manifest
import android.content.*
import android.content.pm.PackageManager
@ -28,14 +27,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
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.helpers.*
import org.y20k.trackbook.ui.MapFragmentLayoutHolder
@ -43,34 +35,34 @@ import org.y20k.trackbook.ui.MapFragmentLayoutHolder
/*
* MapFragment class
*/
class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelper.MarkerListener {
class MapFragment : Fragment(), MapOverlayHelper.MarkerListener
{
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java)
/* Main class variables */
private var bound: Boolean = false
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 networkProviderActive: Boolean = false
private var track: Track = Track()
private lateinit var track: Track
private lateinit var currentBestLocation: Location
private lateinit var layout: MapFragmentLayoutHolder
private lateinit var trackerService: TrackerService
/* Overrides onCreate from Fragment */
override fun onCreate(savedInstanceState: Bundle?) {
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
currentBestLocation = LocationHelper.getLastKnownLocation(activity as Context)
// get saved tracking state
trackingState = PreferencesHelper.loadTrackingState()
}
/* Overrides onStop from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// initialize layout
@ -84,30 +76,10 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
layout.mainButton.setOnClickListener {
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
}
/* Overrides onStart from Fragment */
override fun 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)
}
/* Overrides onResume from Fragment */
override fun onResume() {
super.onResume()
@ -129,7 +100,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
// }
}
/* Overrides onPause from Fragment */
override fun onPause() {
super.onPause()
@ -137,16 +107,19 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) {
trackerService.removeGpsLocationListener()
trackerService.removeNetworkLocationListener()
trackerService.trackbook.database.commit()
}
}
/* Overrides onStop from Fragment */
override fun onStop() {
super.onStop()
// unbind from TrackerService
activity?.unbindService(connection)
handleServiceUnbind()
if (bound)
{
activity?.unbindService(connection)
handleServiceUnbind()
}
}
/* Register the permission launcher for requesting location */
@ -171,14 +144,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
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 */
private fun logPermissionRequestResult(isGranted: Boolean) {
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 */
private fun startTracking() {
// 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)
} else {
}
else
{
// start service via intent so that it keeps running after unbind
startTrackerService()
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 */
private fun startTrackerService() {
private fun startTrackerService()
{
val intent = Intent(activity, TrackerService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// ... 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 */
private fun handleServiceUnbind() {
private fun handleServiceUnbind()
{
bound = false
// unregister listener for changes in shared preferences
PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener)
@ -269,61 +190,25 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
handler.removeCallbacks(periodicLocationRequestRunnable)
}
/* Starts / pauses tracking and toggles the recording sub menu_bottom_navigation */
private fun handleTrackingManagementMenu() {
when (trackingState) {
Keys.STATE_TRACKING_PAUSED -> resumeTracking()
Keys.STATE_TRACKING_ACTIVE -> trackerService.pauseTracking()
Keys.STATE_TRACKING_NOT_STARTED -> startTracking()
}
}
/* Saves track - shows dialog, if recording is still empty */
private fun saveTrack()
private fun handleTrackingManagementMenu()
{
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)
}
}
when (trackingState) {
Keys.STATE_TRACKING_ACTIVE -> trackerService.pauseTracking()
Keys.STATE_TRACKING_STOPPED -> startTracking()
}
}
/* 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
*/
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) {
Keys.PREF_TRACKING_STATE -> {
if (activity != null) {
when (key)
{
Keys.PREF_TRACKING_STATE ->
{
if (activity != null)
{
trackingState = PreferencesHelper.loadTrackingState()
layout.updateMainButton(trackingState)
}
@ -334,7 +219,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
* End of declaration
*/
/*
* Defines callbacks for service binding, passed to bindService()
*/
@ -362,7 +246,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
* End of declaration
*/
/*
* Runnable: Periodically requests location
*/
@ -377,9 +260,11 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
// update location and track
layout.markCurrentPosition(currentBestLocation, 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
if (!layout.userInteraction) { layout.centerMap(currentBestLocation, true)}
if (!layout.userInteraction)
{
layout.centerMap(currentBestLocation, true)
}
// show error snackbar if necessary
layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
// use the handler to start runnable again after specified delay
@ -389,5 +274,4 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
/*
* End of declaration
*/
}

View File

@ -26,6 +26,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.preference.*
import kotlinx.coroutines.CoroutineScope
@ -33,13 +34,13 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
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.FileHelper
import org.y20k.trackbook.helpers.LengthUnitHelper
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
@ -64,6 +65,10 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
val context = preferenceManager.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
val preferenceGpsOnly: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
preferenceGpsOnly.isSingleLineTitle = false
@ -73,6 +78,8 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceGpsOnly.summaryOn = getString(R.string.pref_gps_only_summary_gps_only)
preferenceGpsOnly.summaryOff = getString(R.string.pref_gps_only_summary_gps_and_network)
preferenceGpsOnly.setDefaultValue(false)
preferenceCategoryGeneral.contains(preferenceGpsOnly)
screen.addPreference(preferenceGpsOnly)
// set up "Use Imperial Measurements" preference
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.summaryOff = getString(R.string.pref_imperial_measurement_units_summary_metric)
preferenceImperialMeasurementUnits.setDefaultValue(LengthUnitHelper.useImperialUnits())
preferenceCategoryGeneral.contains(preferenceImperialMeasurementUnits)
screen.addPreference(preferenceImperialMeasurementUnits)
// set up "App Theme" preference
val preferenceThemeSelection: ListPreference = ListPreference(activity as Context)
@ -101,16 +110,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
return@setOnPreferenceChangeListener false
}
}
// 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
}
screen.addPreference(preferenceThemeSelection)
// set up "Recording Accuracy" preference
val DEFAULT_OMIT_RESTS = true
@ -122,46 +122,27 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceOmitRests.summaryOn = getString(R.string.pref_omit_rests_on)
preferenceOmitRests.summaryOff = getString(R.string.pref_omit_rests_off)
preferenceOmitRests.setDefaultValue(DEFAULT_OMIT_RESTS)
preferenceCategoryGeneral.contains(preferenceOmitRests)
screen.addPreference(preferenceOmitRests)
val preferenceAutoExportInterval: SeekBarPreference = SeekBarPreference(activity as Context)
preferenceAutoExportInterval.title = getString(R.string.pref_auto_export_interval_title)
preferenceAutoExportInterval.setIcon(R.drawable.ic_bar_chart_24)
preferenceAutoExportInterval.key = Keys.PREF_AUTO_EXPORT_INTERVAL
preferenceAutoExportInterval.summary = getString(R.string.pref_auto_export_interval_summary)
preferenceAutoExportInterval.showSeekBarValue = true
preferenceAutoExportInterval.min = 1
preferenceAutoExportInterval.max = 24
preferenceAutoExportInterval.setDefaultValue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
val preferenceDeviceID: EditTextPreference = EditTextPreference(activity as Context)
preferenceDeviceID.title = getString(R.string.pref_device_id)
preferenceDeviceID.setIcon(R.drawable.ic_smartphone_24dp)
preferenceDeviceID.key = Keys.PREF_DEVICE_ID
preferenceDeviceID.summary = getString(R.string.pref_device_id_summary) + "\n" + PreferencesHelper.load_device_id()
preferenceDeviceID.setDefaultValue(random_int().toString())
preferenceCategoryGeneral.contains(preferenceDeviceID)
screen.addPreference(preferenceDeviceID)
// set up "Altitude Smoothing" preference
// val preferenceAltitudeSmoothingValue: SeekBarPreference = SeekBarPreference(activity as Context)
// preferenceAltitudeSmoothingValue.title = getString(R.string.pref_altitude_smoothing_value_title)
// 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
}
val preferenceCategoryAbout: PreferenceCategory = PreferenceCategory(context)
preferenceCategoryAbout.title = getString(R.string.pref_about_title)
screen.addPreference(preferenceCategoryAbout)
// set up "App Version" preference
val preferenceAppVersion: Preference = Preference(context)
preferenceAppVersion.title = getString(R.string.pref_app_version_title)
preferenceAppVersion.setIcon(R.drawable.ic_info_24dp)
preferenceAppVersion.summary = "${getString(R.string.pref_app_version_summary)} ${BuildConfig.VERSION_NAME} (${getString(
R.string.app_version_name)})"
preferenceAppVersion.summary = "${getString(R.string.pref_app_version_summary)} ${BuildConfig.VERSION_NAME}"
preferenceAppVersion.setOnPreferenceClickListener {
// copy to clipboard
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()
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)
screen.addPreference(preferenceAppVersion)
// 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
}
@ -212,25 +163,11 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
when (type) {
Keys.DIALOG_DELETE_NON_STARRED -> {
when (dialogResult) {
// user tapped delete
true -> {
deleteNonStarred(activity as Context)
}
}
}
else -> {
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 androidx.activity.result.ActivityResult
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.navigation.fragment.findNavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.y20k.trackbook.core.Database
import org.y20k.trackbook.core.Track
import org.y20k.trackbook.core.track_from_file
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.MapOverlayHelper
import org.y20k.trackbook.helpers.TrackHelper
import org.y20k.trackbook.helpers.iso8601_format
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 {
@ -59,39 +51,24 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
/* Main class variables */
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 */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// initialize layout
val track: Track
if (this::trackFileUriString.isInitialized && trackFileUriString.isNotBlank())
{
track = track_from_file(activity as Context, Uri.parse(trackFileUriString).toFile())
} else {
track = Track()
}
val database: Database = (requireActivity().applicationContext as Trackbook).database
val track: Track = Track(
database=database,
device_id= this.requireArguments().getString(Keys.ARG_TRACK_DEVICE_ID, ""),
start_time= iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_START_TIME)!!),
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)
// set up share button
layout.shareButton.setOnClickListener {
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
layout.deleteButton.setOnClickListener {
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
}
/* Overrides onResume from Fragment */
override fun onResume()
{
super.onResume()
}
/* Overrides onPause from Fragment */
override fun onPause()
{
@ -131,39 +106,26 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
private val requestSaveGpxLauncher = registerForActivityResult(StartActivityForResult(), this::requestSaveGpxResult)
/* Pass the activity result */
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()
Toast.makeText(activity as Context, sourceUri.toString(), Toast.LENGTH_LONG).show()
val targetUri: Uri? = result.data?.data
if (targetUri != null)
{
// copy file async (= fire & forget - no return value needed)
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()
}
return
}
val targetUri: Uri? = result.data?.data
if (targetUri == null)
{
return
}
val outputsuccess: Uri? = layout.track.export_gpx(activity as Context, targetUri)
if (outputsuccess == null)
{
Toast.makeText(activity as Context, "failed to export for some reason", Toast.LENGTH_LONG).show()
}
}
/* Overrides onRenameTrackDialog from RenameTrackDialog */
override fun onRenameTrackDialog(textInput: String)
{
// rename track async (= fire & forget - no return value needed)
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 */
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
true -> {
// switch to TracklistFragment and remove track there
val bundle: Bundle = bundleOf(Keys.ARG_TRACK_ID to layout.track.id)
findNavController().navigate(R.id.tracklist_fragment, bundle)
// val bundle: Bundle = bundleOf(Keys.ARG_TRACK_ID to layout.track.id)
// findNavController().navigate(R.id.tracklist_fragment, bundle)
}
else ->
{
;
}
}
}
}
}
/* Overrides onMarkerTapped from MarkerListener */
override fun onMarkerTapped(latitude: Double, longitude: Double)
{
@ -192,12 +157,11 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
layout.updateTrackOverlay()
}
/* Opens up a file picker to select the save location */
private fun openSaveGpxDialog()
{
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 {
addCategory(Intent.CATEGORY_OPENABLE)
type = Keys.MIME_TYPE_GPX
@ -213,42 +177,5 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
LogHelper.e(TAG, "Unable to save GPX.")
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
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
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 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.LogHelper
import org.y20k.trackbook.helpers.PreferencesHelper
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
*/
class Trackbook: Application() {
class Trackbook(): Application() {
val database: Database = Database()
val homepoints: ArrayList<Homepoint> = ArrayList()
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(Trackbook::class.java)
/* Implements onCreate */
override fun onCreate() {
override fun onCreate()
{
super.onCreate()
LogHelper.v(TAG, "Trackbook application started.")
LogHelper.v("VOUSSOIR", "Trackbook application started.")
DynamicColors.applyToActivitiesIfAvailable(this)
// initialize single sharedPreferences object when app is launched
initPreferences()
// set Dark / Light theme state
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 */
override fun onTerminate() {
fun homepoint_generator() = iterator<Homepoint>
{
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()
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.LocationManager
import android.Manifest
import android.content.ContentValues
import android.os.*
import android.util.Log
import androidx.core.content.ContextCompat
import java.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.Runnable
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 java.text.SimpleDateFormat
/*
* TrackerService class
@ -52,24 +52,25 @@ class TrackerService: Service(), SensorEventListener
private val TAG: String = LogHelper.makeLogTag(TrackerService::class.java)
/* Main class variables */
var trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED
var trackingState: Int = Keys.STATE_TRACKING_STOPPED
var gpsProviderActive: Boolean = false
var networkProviderActive: Boolean = false
var useImperial: Boolean = false
var gpsOnly: Boolean = false
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 lastTempSave: Date = Keys.DEFAULT_DATE
var lastAutoExport: Date = Keys.DEFAULT_DATE
var lastCommit: Date = Keys.DEFAULT_DATE
var stepCountOffset: Float = 0f
var track: Track = Track()
lateinit var track: Track
var gpsLocationListenerRegistered: Boolean = false
var networkLocationListenerRegistered: Boolean = false
var bound: Boolean = false
private val binder = LocalBinder()
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 sensorManager: SensorManager
private lateinit var notificationManager: NotificationManager
@ -111,7 +112,8 @@ class TrackerService: Service(), SensorEventListener
}
/* Adds a Network location listener to location manager */
private fun addNetworkLocationListener() {
private fun addNetworkLocationListener()
{
if (gpsOnly)
{
LogHelper.v(TAG, "Skipping Network listener. User prefers GPS-only.")
@ -150,12 +152,11 @@ class TrackerService: Service(), SensorEventListener
fun clearTrack()
{
track = Track()
stepCountOffset = 0f
FileHelper.delete_temp_file(this as Context)
trackingState = Keys.STATE_TRACKING_NOT_STARTED
trackingState = Keys.STATE_TRACKING_STOPPED
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
}
@ -205,9 +206,7 @@ class TrackerService: Service(), SensorEventListener
private fun displayNotification(): Notification {
val notification: Notification = notificationHelper.createNotification(
trackingState,
track.distance,
track.duration,
useImperial
iso8601(GregorianCalendar.getInstance().time)
)
notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification)
return notification
@ -233,11 +232,14 @@ class TrackerService: Service(), SensorEventListener
override fun onCreate()
{
super.onCreate()
trackbook = (applicationContext as Trackbook)
trackbook.load_homepoints()
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()
omitRests = PreferencesHelper.loadOmitRests()
autoExportInterval = PreferencesHelper.loadAutoExportInterval()
commitInterval = PreferencesHelper.loadCommitInterval()
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -248,8 +250,6 @@ class TrackerService: Service(), SensorEventListener
networkLocationListener = createLocationListener()
trackingState = PreferencesHelper.loadTrackingState()
currentBestLocation = LocationHelper.getLastKnownLocation(this)
track = load_temp_track(this)
// altitudeValues.capacity = PreferencesHelper.loadAltitudeSmoothingValue()
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
}
@ -282,13 +282,11 @@ class TrackerService: Service(), SensorEventListener
if (sensorEvent != null) {
if (stepCountOffset == 0f) {
// 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
steps = sensorEvent.values[0] - stepCountOffset
}
// update step count in track
track.stepCount = steps
}
/* Overrides onStartCommand from Service */
@ -300,7 +298,7 @@ class TrackerService: Service(), SensorEventListener
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
{
LogHelper.w(TAG, "Trackbook has been killed by the operating system. Trying to resume recording.")
resumeTracking()
startTracking()
}
}
else if (intent.action == Keys.ACTION_STOP)
@ -311,10 +309,6 @@ class TrackerService: Service(), SensorEventListener
{
startTracking()
}
else if (intent.action == Keys.ACTION_RESUME)
{
resumeTracking()
}
// START_STICKY is used for services that are explicitly started and stopped as needed
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()
{
val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI)
if (!stepCounterAvailable) {
LogHelper.w(TAG, "Pedometer sensor not available.")
track.stepCount = -1f
}
}
@ -401,12 +364,11 @@ class TrackerService: Service(), SensorEventListener
{
addGpsLocationListener()
addNetworkLocationListener()
// set up new track
if (newTrack) {
track = Track()
stepCountOffset = 0f
}
trackingState = Keys.STATE_TRACKING_ACTIVE
if (newTrack)
{
this.recording_started = GregorianCalendar.getInstance().time
}
PreferencesHelper.saveTrackingState(trackingState)
startStepCounter()
handler.postDelayed(periodicTrackUpdate, 0)
@ -415,19 +377,16 @@ class TrackerService: Service(), SensorEventListener
fun pauseTracking()
{
track.recordingStop = GregorianCalendar.getInstance().time
CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) }
trackbook.database.commit()
trackingState = Keys.STATE_TRACKING_PAUSED
trackingState = Keys.STATE_TRACKING_STOPPED
PreferencesHelper.saveTrackingState(trackingState)
altitudeValues.reset()
sensorManager.unregisterListener(this)
handler.removeCallbacks(periodicTrackUpdate)
displayNotification()
stopForeground(false)
stopForeground(STOP_FOREGROUND_DETACH)
}
/*
@ -435,24 +394,25 @@ class TrackerService: Service(), SensorEventListener
*/
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) {
// preference "Restrict to GPS"
Keys.PREF_GPS_ONLY -> {
Keys.PREF_GPS_ONLY ->
{
gpsOnly = PreferencesHelper.loadGpsOnly()
when (gpsOnly) {
true -> removeNetworkLocationListener()
false -> addNetworkLocationListener()
}
}
// preference "Use Imperial Measurements"
Keys.PREF_USE_IMPERIAL_UNITS -> {
Keys.PREF_USE_IMPERIAL_UNITS ->
{
useImperial = PreferencesHelper.loadUseImperialUnits()
}
// preference "Recording Accuracy"
Keys.PREF_OMIT_RESTS -> {
Keys.PREF_OMIT_RESTS ->
{
omitRests = PreferencesHelper.loadOmitRests()
}
Keys.PREF_AUTO_EXPORT_INTERVAL -> {
autoExportInterval = PreferencesHelper.loadAutoExportInterval()
Keys.PREF_DEVICE_ID ->
{
device_id = PreferencesHelper.load_device_id()
}
}
}
@ -470,84 +430,96 @@ class TrackerService: Service(), SensorEventListener
* 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)
*/
private val periodicTrackUpdate: Runnable = object : Runnable
{
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
if (success) {
track.resumed = false
// store previous smoothed altitude
val previousAltitude: Double = altitudeValues.getAverage()
// put current altitude into queue
val currentBestLocationAltitude: Double = currentBestLocation.altitude
if (currentBestLocationAltitude != Keys.DEFAULT_ALTITUDE) altitudeValues.add(currentBestLocationAltitude)
// TODO remove
// uncomment to use test altitude values - useful if testing with an emulator
//altitudeValues.add(getTestAltitude()) // TODO remove
// TODO remove
// only start calculating elevation differences, if enough data has been added to queue
if (altitudeValues.prepared) {
// get current smoothed altitude
val currentAltitude: Double = altitudeValues.getAverage()
// calculate and store elevation differences
track = LocationHelper.calculateElevationDifferences(currentAltitude, previousAltitude, track)
// TODO remove
LogHelper.d(TAG, "Elevation Calculation || prev = $previousAltitude | curr = $currentAltitude | pos = ${track.positiveElevation} | neg = ${track.negativeElevation}")
// TODO remove
val nowstr: String = iso8601(now)
val trkpt: Trkpt = Trkpt(location=currentBestLocation)
Log.i("VOUSSOIR", "Processing point ${currentBestLocation.latitude}, ${currentBestLocation.longitude} ${nowstr}.")
if (should_keep_point((currentBestLocation)))
{
val values = ContentValues().apply {
put("device_id", device_id)
put("lat", trkpt.latitude)
put("lon", trkpt.longitude)
put("time", nowstr)
put("accuracy", trkpt.accuracy)
put("sat", trkpt.numberSatellites)
put("ele", trkpt.altitude)
put("star", 0)
}
if (! trackbook.database.connection.inTransaction())
{
trackbook.database.connection.beginTransaction()
}
trackbook.database.connection.insert("trkpt", null, values)
track.trkpts.add(trkpt)
if (track.trkpts.size > track.dequelimit)
{
track.trkpts.removeFirst()
}
// save a temp track
if (now.time - lastTempSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) {
lastTempSave = now
CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) }
if (now.time - lastCommit.time > Keys.SAVE_TEMP_TRACK_INTERVAL)
{
if (trackbook.database.connection.inTransaction())
{
trackbook.database.commit()
}
lastCommit = now
}
}
if (now.time - track.recordingStart.time > (autoExportInterval * Keys.ONE_HOUR_IN_MILLISECONDS)) {
saveTrackAndStartNew(this@TrackerService)
}
// update notification
displayNotification()
// re-run this in set interval
handler.postDelayed(this, Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL)
handler.postDelayed(this, Keys.TRACKING_INTERVAL)
}
}
/*
* 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 */
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

View File

@ -37,6 +37,7 @@ import kotlinx.coroutines.Dispatchers.Main
import org.y20k.trackbook.core.Track
import org.y20k.trackbook.helpers.LogHelper
import org.y20k.trackbook.helpers.UiHelper
import org.y20k.trackbook.helpers.iso8601_format
import org.y20k.trackbook.tracklist.TracklistAdapter
@ -48,21 +49,18 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TracklistFragment::class.java)
/* Main class variables */
private lateinit var tracklistAdapter: TracklistAdapter
private lateinit var trackElementList: RecyclerView
private lateinit var tracklistOnboarding: ConstraintLayout
/* Overrides onCreateView from Fragment */
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// create tracklist adapter
tracklistAdapter = TracklistAdapter(this)
tracklistAdapter = TracklistAdapter(this, (requireActivity().applicationContext as Trackbook).database)
}
/* Overrides onCreateView from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// find views
@ -97,9 +95,9 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
override fun onTrackElementTapped(track: Track) {
val bundle: Bundle = bundleOf(
Keys.ARG_TRACK_TITLE to track.name,
Keys.ARG_TRACK_FILE_URI to track.get_json_file(activity as Context).toUri().toString(),
Keys.ARG_GPX_FILE_URI to track.get_gpx_file(activity as Context).toUri().toString(),
Keys.ARG_TRACK_ID to track.id
Keys.ARG_TRACK_DEVICE_ID to track.device_id,
Keys.ARG_TRACK_START_TIME to iso8601_format.format(track.start_time),
Keys.ARG_TRACK_STOP_TIME to iso8601_format.format(track.stop_time),
)
findNavController().navigate(R.id.fragment_track, bundle)
}
@ -130,7 +128,6 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
}
}
// toggle onboarding layout
private fun toggleOnboardingLayout() {
when (tracklistAdapter.isEmpty()) {
@ -147,13 +144,11 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
}
}
/*
* 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 {
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
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.database.Cursor
import android.location.Location
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.Log
import android.widget.Toast
import androidx.annotation.Keep
import androidx.core.app.ActivityCompat
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.R
import org.y20k.trackbook.helpers.DateTimeHelper
import org.y20k.trackbook.helpers.FileHelper
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.net.URI
import java.text.SimpleDateFormat
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
/*
* Track data class
*/
@Keep
@Parcelize
data class Track (
@Expose val id: Long = make_random_id(),
@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
@Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(),
@Expose var distance: Float = 0f,
@Expose var duration: Long = 0L,
@Expose var recordingPaused: Long = 0L,
@Expose var stepCount: Float = 0f,
@Expose var recordingStart: Date = GregorianCalendar.getInstance().time,
@Expose var dateString: String = DateTimeHelper.convertToReadableDate(recordingStart),
@Expose var recordingStop: Date = recordingStart,
// The resumed flag will be true for the first point that is received after unpausing a
// recording, so that the distance travelled while paused is not added to the track.distance.
@Expose var resumed: Boolean = false,
@Expose var maxAltitude: Double = 0.0,
@Expose var 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
val database: Database,
val device_id: String,
val start_time: Date,
val stop_time: Date,
var name: String = "",
var dequelimit: Int = 7200,
var view_latitude: Double = Keys.DEFAULT_LATITUDE,
var view_longitude: Double = Keys.DEFAULT_LONGITUDE,
var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
val trkpts: ArrayDeque<Trkpt> = ArrayDeque<Trkpt>(dequelimit),
var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL,
)
{
fun add_waypoint(location: Location, omitRests: Boolean, resumed: Boolean): Boolean
fun delete()
{
// 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
}
// Step 3.1: Update distance (do not update if resumed -> we do not want to add values calculated during a recording pause)
if (!resumed)
{
this.distance = this.distance + LocationHelper.calculateDistance(previousLocation, location)
}
// Step 3.2: Update altitude values
val altitude: Double = location.altitude
if (altitude != 0.0)
{
if (numberOfWayPoints == 0)
{
this.maxAltitude = altitude
this.minAltitude = altitude
}
else
{
if (altitude > this.maxAltitude) this.maxAltitude = altitude
if (altitude < this.minAltitude) this.minAltitude = altitude
}
}
// Step 3.3: Toggle stop over status, if necessary
if (this.wayPoints.size < 0)
{
this.wayPoints[this.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location)
}
// Step 3.4: Add current location as point to center on for later display
this.latitude = location.latitude
this.longitude = location.longitude
// Step 3.5: Add location as new waypoint
this.wayPoints.add(WayPoint(location = location, distanceToStartingPoint = this.distance))
return true
}
fun delete(context: Context)
{
Log.i("VOUSSOIR", "Deleting track ${this.id}.")
val json_file: File = this.get_json_file(context)
if (json_file.isFile)
{
json_file.delete()
}
val gpx_file: File = this.get_gpx_file(context)
if (gpx_file.isFile)
{
gpx_file.delete()
}
}
suspend fun delete_suspended(context: Context)
{
return suspendCoroutine { cont ->
cont.resume(this.delete(context))
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
{
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)
}
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
return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), basename)
}
fun save_all_files(context: Context)
{
this.save_json(context)
this.save_gpx(context)
this.save_export_gpx(context)
}
suspend fun save_all_files_suspended(context: Context)
{
return suspendCoroutine { cont ->
cont.resume(this.save_all_files(context))
if (! database.ready)
{
Log.i("VOUSSOIR", "Failed to export due to database not ready.")
return null
}
}
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))
Log.i("VOUSSOIR", "Let's export to " + fileuri.toString())
val writer = context.contentResolver.openOutputStream(fileuri)
if (writer == null)
{
return null
}
}
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
gpxString.appendLine("""
val write = {x: String -> writer.write(x.encodeToByteArray()); writer.write("\n".encodeToByteArray())}
write("""
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<gpx
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"
>
""".trimIndent())
gpxString.appendLine("\t<metadata>")
gpxString.appendLine("\t\t<name>Trackbook Recording: ${this.name}</name>")
gpxString.appendLine("\t</metadata>")
// POIs
val poiList: List<WayPoint> = this.wayPoints.filter { it.starred }
poiList.forEach { poi ->
gpxString.appendLine("\t<wpt lat=\"${poi.latitude}\" lon=\"${poi.longitude}\">")
gpxString.appendLine("\t\t<name>Point of interest</name>")
gpxString.appendLine("\t\t<ele>${poi.altitude}</ele>")
gpxString.appendLine("\t</wpt>")
}
write("\t<metadata>")
write("\t\t<name>Trackbook Recording: ${this.name}</name>")
write("\t\t<device>${this.device_id}</device>")
write("\t</metadata>")
// TRK
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
gpxString.appendLine("\t<trk>")
gpxString.appendLine("\t\t<name>${this.name}</name>")
gpxString.appendLine("\t\t<trkseg>")
this.wayPoints.forEach { wayPoint ->
gpxString.appendLine("\t\t\t<trkpt lat=\"${wayPoint.latitude}\" lon=\"${wayPoint.longitude}\">")
gpxString.appendLine("\t\t\t\t<ele>${wayPoint.altitude}</ele>")
gpxString.appendLine("\t\t\t\t<time>${dateFormat.format(Date(wayPoint.time))}</time>")
gpxString.appendLine("\t\t\t\t<sat>${wayPoint.numberSatellites}</sat>")
gpxString.appendLine("\t\t\t</trkpt>")
write("\t<trk>")
write("\t\t<name>${this.name}</name>")
write("\t\t<trkseg>")
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>")
}
gpxString.appendLine("\t\t</trkseg>")
gpxString.appendLine("\t</trk>")
gpxString.appendLine("</gpx>")
return gpxString.toString()
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 to_json(): String
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
{
return track_from_file(context, FileHelper.get_temp_file(context))
}
fun track_from_file(context: Context, file: File): Track
{
// get JSON from text file
val json: String = FileHelper.readTextFile(context, file)
if (json.isEmpty())
fun statistics(): TrackStatistics
{
return Track()
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 trkpt_generator() = iterator<Trkpt>
{
val cursor: Cursor = database.connection.query(
"trkpt",
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
{
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
{
return (Random.nextBits(31).toLong() shl 32) + Random.nextBits(32)
}
data class TrackStatistics(
var distance: Double = 0.0,
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 kotlinx.parcelize.Parcelize
import org.y20k.trackbook.helpers.LocationHelper
import java.util.*
/*
* WayPoint data class
*/
@Keep
@Parcelize
data class WayPoint(@Expose val provider: String,
data class Trkpt(@Expose val provider: String,
@Expose val latitude: Double,
@Expose val longitude: Double,
@Expose val altitude: Double,
@Expose val accuracy: Float,
@Expose val time: Long,
@Expose val time: Date,
@Expose val distanceToStartingPoint: Float = 0f,
@Expose val numberSatellites: Int = 0,
@Expose var isStopOver: Boolean = false,
@ -43,28 +43,16 @@ data class WayPoint(@Expose val provider: String,
/* Constructor using just Location */
constructor(location: Location) : this (
provider=location.provider,
provider=location.provider.toString(),
latitude=location.latitude,
longitude=location.longitude,
altitude=location. altitude,
altitude=location.altitude,
accuracy=location.accuracy,
time=location.time,
time=Date(location.time),
distanceToStartingPoint=0F,
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 */
fun toLocation(): Location {
val location: Location = Location(provider)
@ -72,7 +60,7 @@ data class WayPoint(@Expose val provider: String,
location.longitude = longitude
location.altitude = altitude
location.accuracy = accuracy
location.time = time
location.time = this.time.time
return location
}

View File

@ -38,31 +38,6 @@ object FileHelper {
/* Define log tag */
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 fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
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 */
fun convertToVelocityString(trackDuration: Long, trackRecordingPause: Long, trackLength: Float, useImperialUnits: Boolean = false) : String {
fun convertToVelocityString(velocity: Double, useImperialUnits: Boolean = false) : String {
var speed: String = "0"
// duration minus pause in seconds
val duration: Long = (trackDuration - trackRecordingPause) / 1000L
if (duration > 0L) {
if (velocity > 0.0) {
// speed in km/h / mph
val velocity: Double = convertMetersPerSecond((trackLength / duration), useImperialUnits)
val velocity: Double = convertMetersPerSecond(velocity, useImperialUnits)
// create readable speed string
var bd: BigDecimal = BigDecimal.valueOf(velocity)
bd = bd.setScale(1, RoundingMode.HALF_UP)
@ -119,7 +116,7 @@ object LengthUnitHelper {
/* 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) {
// mph
return metersPerSecond * 2.2369362920544

View File

@ -51,14 +51,6 @@ object LocationHelper {
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 */
fun getLastKnownLocation(context: Context): Location {
// get last location that Trackbook has stored
@ -117,7 +109,6 @@ object LocationHelper {
}
}
/* Checks if GPS location provider is available and enabled */
fun isGpsEnabled(locationManager: LocationManager): Boolean {
if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) {
@ -156,27 +147,6 @@ object LocationHelper {
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 */
fun isDifferentEnough(previousLocation: Location?, location: Location, omitRests: Boolean): Boolean {
// check if previous location is (not) available
@ -216,51 +186,6 @@ object LocationHelper {
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 */
fun getNumberOfSatellites(location: Location): 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.R
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.SimpleDateFormat
import java.util.*
@ -58,10 +58,10 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
/* 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 locationIsOld:Boolean = LocationHelper.isOldLocation(location)
val locationIsOld: Boolean = !(LocationHelper.isRecentEnough(location))
// create marker
val newMarker: Drawable
@ -83,7 +83,24 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
}
// 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)
overlayItems.add(overlayItem)
@ -93,13 +110,14 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
/* 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
val color = if (trackingState == Keys.STATE_TRACKING_ACTIVE) context.getColor(R.color.default_red)
else context.getColor(R.color.default_blue)
// gather points for overlay
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})"
// only add normal points
if (!wayPoint.starred && !wayPoint.isStopOver) {
@ -133,45 +151,60 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
return overlay
}
/* 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 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
if (!trackingActive && index == 0 && displayStartEndMarker && wayPoint.starred) {
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time)
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_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)!!)
} 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)!!)
} 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)!!)
} 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)!!)
} 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)!!)
} 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)!!)
}
// 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
return createOverlay(context, overlayItems, enableStarring = true)
}
/* Creates a marker overlay item */
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)}"
@ -183,7 +216,6 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
return item
}
/* Creates an overlay */
private fun createOverlay(context: Context, overlayItems: ArrayList<OverlayItem>, enableStarring: Boolean): ItemizedIconOverlay<OverlayItem> {
return ItemizedIconOverlay<OverlayItem>(context, overlayItems,

View File

@ -45,7 +45,7 @@ class NotificationHelper(private val trackerService: TrackerService) {
/* 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
if (shouldCreateNotificationChannel()) {
@ -56,7 +56,7 @@ class NotificationHelper(private val trackerService: TrackerService) {
val builder = NotificationCompat.Builder(trackerService, Keys.NOTIFICATION_CHANNEL_RECORDING)
builder.setContentIntent(showActionPendingIntent)
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
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 */
private fun shouldCreateNotificationChannel() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists()
/* Checks if notification channel exists */
@RequiresApi(Build.VERSION_CODES.O)
private fun nowPlayingChannelExists() = notificationManager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null
/* Create a notification channel */
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
@ -103,20 +94,24 @@ class NotificationHelper(private val trackerService: TrackerService) {
notificationManager.createNotificationChannel(notificationChannel)
}
/* Notification pending intents */
private val stopActionPendingIntent = PendingIntent.getService(
trackerService,14,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP),PendingIntent.FLAG_IMMUTABLE)
trackerService,
14,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP),
PendingIntent.FLAG_IMMUTABLE
)
private val resumeActionPendingIntent = PendingIntent.getService(
trackerService, 16,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME),PendingIntent.FLAG_IMMUTABLE)
trackerService,
16,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME),
PendingIntent.FLAG_IMMUTABLE
)
private val showActionPendingIntent: PendingIntent? = TaskStackBuilder.create(trackerService).run {
addNextIntentWithParentStack(Intent(trackerService, MainActivity::class.java))
getPendingIntent(10, PendingIntent.FLAG_IMMUTABLE)
}
/* Notification actions */
private val stopAction = NotificationCompat.Action(
R.drawable.ic_notification_action_stop_24dp,

View File

@ -20,6 +20,7 @@ import android.content.Context
import android.content.SharedPreferences
import android.location.Location
import android.location.LocationManager
import android.util.Log
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import org.y20k.trackbook.Keys
@ -43,6 +44,13 @@ object PreferencesHelper {
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 {
return sharedPreferences.getDouble(Keys.PREF_MAP_ZOOM_LEVEL, Keys.DEFAULT_ZOOM_LEVEL)
}
@ -52,7 +60,7 @@ object PreferencesHelper {
}
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) {
@ -71,14 +79,10 @@ object PreferencesHelper {
return sharedPreferences.getBoolean(Keys.PREF_OMIT_RESTS, true)
}
fun loadAutoExportInterval(): Int {
return sharedPreferences.getInt(Keys.PREF_AUTO_EXPORT_INTERVAL, Keys.DEFAULT_AUTO_EXPORT_INTERVAL)
fun loadCommitInterval(): Int {
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 */
fun loadCurrentBestLocation(): Location {
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.widget.Toast
import java.util.*
import org.y20k.trackbook.R
import org.y20k.trackbook.core.Track
@ -35,7 +34,7 @@ object TrackHelper {
/* Toggles starred flag for given position */
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) {
waypoint.starred = !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.database.Cursor
import android.util.Log
import android.view.LayoutInflater
import android.view.View
@ -26,49 +27,72 @@ import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DiffUtil
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.R
import org.y20k.trackbook.core.Database
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 java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/*
* TracklistAdapter class
*/
class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TracklistAdapter::class.java)
class TracklistAdapter(val fragment: Fragment, val database: Database) : RecyclerView.Adapter<RecyclerView.ViewHolder>()
{
/* Main class variables */
private val context: Context = fragment.activity as Context
private lateinit var tracklistListener: TracklistAdapterListener
private var useImperial: Boolean = PreferencesHelper.loadUseImperialUnits()
private var tracklist: Tracklist = Tracklist()
val tracks: ArrayList<Track> = ArrayList<Track>()
/* Listener Interface */
interface TracklistAdapterListener {
interface TracklistAdapterListener
{
fun onTrackElementTapped(track: Track) { }
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView)
{
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 */
override fun getItemViewType(position: Int): Int {
override fun getItemViewType(position: Int): Int
{
return Keys.VIEW_TYPE_TRACK
}
/* Overrides getItemCount from RecyclerView.Adapter */
override fun getItemCount(): Int {
return tracklist.tracks.size
override fun getItemCount(): Int
{
return tracks.size
}
@ -97,17 +123,10 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
{
val positionInTracklist: Int = position
val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder
elementTrackViewHolder.trackNameView.text = tracklist.tracks[positionInTracklist].name
elementTrackViewHolder.trackNameView.text = getTrackName(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 {
tracklistListener.onTrackElementTapped(tracklist.tracks[positionInTracklist])
}
elementTrackViewHolder.starButton.setOnClickListener {
toggleStarred(it, positionInTracklist)
tracklistListener.onTrackElementTapped(tracks[positionInTracklist])
}
}
@ -115,16 +134,16 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/* Get track name for given position */
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)
{
val track = tracklist.tracks[index]
track.delete(context)
tracklist.tracks.remove(track)
notifyItemRemoved(index)
notifyItemRangeChanged(index, this.itemCount);
// val track = tracklist.tracks[index]
// track.delete()
// tracklist.tracks.remove(track)
// notifyItemRemoved(index)
// notifyItemRangeChanged(index, this.itemCount);
}
suspend fun delete_track_at_position_suspended(context: Context, position: Int)
@ -136,81 +155,40 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
fun delete_track_by_id(context: Context, trackId: Long)
{
val index: Int = tracklist.tracks.indexOfFirst {it.id == trackId}
if (index == -1) {
return
}
tracklist.tracks[index].delete(context)
tracklist.tracks.removeAt(index)
notifyItemRemoved(index)
notifyItemRangeChanged(index, this.itemCount);
// val index: Int = tracklist.tracks.indexOfFirst {it.id == trackId}
// if (index == -1) {
// return
// }
// tracklist.tracks[index].delete()
// tracklist.tracks.removeAt(index)
// notifyItemRemoved(index)
// notifyItemRangeChanged(index, this.itemCount);
}
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))
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)
fun isEmpty(): Boolean
{
return tracks.size == 0
}
/* Creates the track data string */
private fun createTrackDataString(position: Int): String {
val track: Track = tracklist.tracks[position]
val track_duration_string = DateTimeHelper.convertToReadableTime(context, track.duration)
val trackDataString: String
when (track.name == track.dateString) {
// CASE: no individual name set - exclude date
true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)}${track_duration_string}"
// CASE: no individual name set - include date
false -> trackDataString = "${track.dateString}${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)}${track_duration_string}"
}
return trackDataString
private fun createTrackDataString(position: Int): String
{
val track: Track = tracks[position]
return "device: " + track.device_id
// val track_duration_string = DateTimeHelper.convertToReadableTime(context, track.duration)
// val trackDataString: String
// if (track.name == track.dateString)
// {
// trackDataString = "${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
// }
// else
// {
// trackDataString = "${track.dateString} • ${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
// }
// 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
*/
/*
* Inner class: ViewHolder for a track element
*/
@ -218,8 +196,6 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
val trackElement: ConstraintLayout = elementTrackLayout.findViewById(R.id.track_element)
val trackNameView: TextView = elementTrackLayout.findViewById(R.id.track_name)
val trackDataView: TextView = elementTrackLayout.findViewById(R.id.track_data)
val starButton: ImageButton = elementTrackLayout.findViewById(R.id.star_button)
}
/*
* 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.y20k.trackbook.Keys
import org.y20k.trackbook.R
import org.y20k.trackbook.Trackbook
import org.y20k.trackbook.core.Track
import org.y20k.trackbook.helpers.*
@ -66,17 +67,11 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
var userInteraction: Boolean = false
val currentLocationButton: FloatingActionButton
val mainButton: ExtendedFloatingActionButton
val saveButton: FloatingActionButton
val clearButton: FloatingActionButton
private val additionalButtons: Group
private val mapView: MapView
private val homepoint_overlays: ArrayList<ItemizedIconOverlay<OverlayItem>> = ArrayList()
private var currentPositionOverlay: ItemizedIconOverlay<OverlayItem>
private var currentTrackOverlay: SimpleFastPointOverlay?
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 var locationErrorBar: Snackbar
private var controller: IMapController
@ -90,13 +85,6 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
mapView = rootView.findViewById(R.id.map)
currentLocationButton = rootView.findViewById(R.id.location_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)
// basic map setup
@ -124,11 +112,8 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
compassOverlay.setCompassCenter((screen_width / densityScalingFactor) - 36f, 36f)
mapView.overlays.add(compassOverlay)
// position the live statistics
(liveStatisticsDistanceView.layoutParams as ConstraintLayout.LayoutParams).apply {
// topMargin = (12 * densityScalingFactor).toInt() + statusBarHeight // TODO uncomment when transparent status bar is re-implemented
topMargin = (12 * densityScalingFactor).toInt()
}
val app: Trackbook = (context.applicationContext as Trackbook)
app.homepoint_generator().forEach { homepoint -> mapView.overlays.add(MapOverlayHelper(markerListener).createHomepointOverlay(context, homepoint.location))}
// add my location overlay
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 */
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)
currentPositionOverlay = MapOverlayHelper(markerListener).createMyLocationOverlay(context, location, trackingState)
mapView.overlays.add(currentPositionOverlay)
@ -193,7 +178,7 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
if (currentTrackSpecialMarkerOverlay != null) {
mapView.overlays.remove(currentTrackSpecialMarkerOverlay)
}
if (track.wayPoints.isNotEmpty()) {
if (track.trkpts.isNotEmpty()) {
val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener)
currentTrackOverlay = mapOverlayHelper.createTrackOverlay(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) */
fun updateMainButton(trackingState: Int)
{
when (trackingState) {
Keys.STATE_TRACKING_NOT_STARTED -> {
Keys.STATE_TRACKING_STOPPED -> {
mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp)
mainButton.text = context.getString(R.string.button_start)
mainButton.contentDescription = context.getString(R.string.descr_button_start)
additionalButtons.isGone = true
currentLocationButton.isVisible = true
}
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.contentDescription = context.getString(R.string.descr_button_start)
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
mainButton.contentDescription = context.getString(R.string.descr_button_pause)
currentLocationButton.isVisible = true
}
}

View File

@ -30,9 +30,6 @@ import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import com.google.android.material.bottomsheet.BottomSheetBehavior
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.IMapController
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.R
import org.y20k.trackbook.core.Track
import org.y20k.trackbook.core.TrackStatistics
import org.y20k.trackbook.helpers.*
import kotlin.math.roundToInt
@ -58,12 +56,14 @@ import kotlin.math.roundToInt
* 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 container: ViewGroup?, var track: Track): MapListener {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackFragmentLayoutHolder::class.java)
data class TrackFragmentLayoutHolder(
private var context: Context,
private var markerListener: MapOverlayHelper.MarkerListener,
private var inflater: LayoutInflater,
private var container: ViewGroup?,
var track: Track
): MapListener
{
/* Main class variables */
val rootView: View
val shareButton: ImageButton
@ -78,7 +78,6 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
private val statisticsSheetBehavior: BottomSheetBehavior<View>
private val statisticsSheet: NestedScrollView
private val statisticsView: View
private val trackidView: MaterialTextView
private val distanceView: MaterialTextView
private val stepsTitleView: MaterialTextView
private val stepsView: MaterialTextView
@ -115,13 +114,12 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
mapView.setTileSource(TileSourceFactory.MAPNIK)
mapView.setMultiTouchControls(true)
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)
// get views for statistics sheet
statisticsSheet = rootView.findViewById(R.id.statistics_sheet)
statisticsView = rootView.findViewById(R.id.statistics_view)
trackidView = rootView.findViewById(R.id.statistics_data_trackid)
distanceView = rootView.findViewById(R.id.statistics_data_distance)
stepsTitleView = rootView.findViewById(R.id.statistics_p_steps)
stepsView = rootView.findViewById(R.id.statistics_data_steps)
@ -156,9 +154,9 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
// create map overlay
val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener)
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED)
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED, displayStartEndMarker = true)
if (track.wayPoints.isNotEmpty()) {
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED)
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED, displayStartEndMarker = true)
if (track.trkpts.isNotEmpty()) {
mapView.overlays.add(trackSpecialMarkersOverlay)
mapView.overlays.add(trackOverlay)
}
@ -172,77 +170,48 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
/* Updates map overlay */
fun updateTrackOverlay() {
fun updateTrackOverlay()
{
if (trackOverlay != null) {
mapView.overlays.remove(trackOverlay)
}
if (trackSpecialMarkersOverlay != null) {
mapView.overlays.remove(trackSpecialMarkersOverlay)
}
if (track.wayPoints.isNotEmpty()) {
if (track.trkpts.isNotEmpty()) {
val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener)
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED)
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED, displayStartEndMarker = true)
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED)
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED, displayStartEndMarker = true)
mapView.overlays.add(trackOverlay)
mapView.overlays.add(trackSpecialMarkersOverlay)
}
// save track
CoroutineScope(Dispatchers.IO).launch { track.save_all_files_suspended(context) }
}
/* Saves zoom level and center of this map */
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 */
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
}
private fun setupStatisticsViews()
{
// populate views
val stats: TrackStatistics = track.statistics()
trackNameView.text = track.name
trackidView.text = track.id.toString()
distanceView.text = LengthUnitHelper.convertDistanceToString(track.distance, useImperialUnits)
stepsView.text = steps
waypointsView.text = track.wayPoints.size.toString()
durationView.text = DateTimeHelper.convertToReadableTime(context, track.duration)
velocityView.text = LengthUnitHelper.convertToVelocityString(track.duration, track.recordingPaused, track.distance, useImperialUnits)
recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStart)
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStop)
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits)
minAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.minAltitude, 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
}
distanceView.text = LengthUnitHelper.convertDistanceToString(stats.distance, useImperialUnits)
waypointsView.text = track.trkpts.size.toString()
durationView.text = DateTimeHelper.convertToReadableTime(context, stats.duration)
velocityView.text = LengthUnitHelper.convertToVelocityString(stats.velocity, useImperialUnits)
recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.start_time)
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.stop_time)
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.max_altitude, useImperialUnits)
minAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.min_altitude, useImperialUnits)
positiveElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_ascent, useImperialUnits)
negativeElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_descent, useImperialUnits)
// inform user about possible accuracy issues with altitude measurements
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 */
private fun getStatisticsSheetCallback(): BottomSheetBehavior.BottomSheetCallback {
return object : BottomSheetBehavior.BottomSheetCallback() {
@ -313,8 +281,8 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
return false
} else {
val center: IGeoPoint = mapView.mapCenter
track.latitude = center.latitude
track.longitude = center.longitude
track.view_latitude = center.latitude
track.view_longitude = center.longitude
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:textColor="@color/text_default"
app:layout_constraintBottom_toTopOf="@+id/track_data"
app:layout_constraintEnd_toStartOf="@+id/star_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/sample_text_track_name" />
@ -37,28 +36,17 @@
android:id="@+id/track_data"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/text_lightweight"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/track_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/track_name"
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.cardview.widget.CardView>

View File

@ -22,40 +22,11 @@
app:layout_dodgeInsetEdges="bottom">
<!-- 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 -->
<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 -->
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/main_button"
android:layout_width="wrap_content"
@ -76,69 +47,19 @@
android:id="@+id/location_button"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="64dp"
android:layout_height="56dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/descr_button_location"
android:src="@drawable/ic_current_location_24dp"
app:backgroundTint="@color/location_button_background"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
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 -->
<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.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -74,30 +74,6 @@
app:layout_constraintTop_toTopOf="@+id/statistics_track_name_headline"
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
android:id="@+id/statistics_p_distance"
android:layout_width="wrap_content"
@ -108,7 +84,7 @@
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/text_lightweight"
app:layout_constraintStart_toStartOf="@+id/statistics_track_name_headline"
app:layout_constraintTop_toBottomOf="@+id/statistics_p_trackid" />
app:layout_constraintTop_toBottomOf="@+id/statistics_track_name_headline" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/statistics_data_distance"

View File

@ -18,7 +18,6 @@
<!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Ryd</string>
<string name="button_save">Gem</string>
<string name="button_resume">Fortsæt</string>
<!-- dialogs -->
<string name="dialog_share_gpx">Del GPX fil med</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="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_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_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>

View File

@ -18,7 +18,6 @@
<!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Zurücksetzen</string>
<string name="button_save">Speichern</string>
<string name="button_resume">Fortsetzen</string>
<!-- dialogs -->
<string name="dialog_share_gpx">GPX-Datei teilen mit</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_added">Markierung für Ort von Interesse hinzugefügt.</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>

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="button_delete">Limpiar</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_details_button">Mostrar detalles</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_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_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_reset_advanced_summary">Restablece la configuración avanzada a los valores predeterminados.</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>
<!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Supprimer</string>
<string name="button_resume">Reprendre</string>
<string name="button_save">Sauvegarder</string>
<!-- dialogs -->
<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_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="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="descr_button_location">Centrer sur la position actuelle</string>
</resources>

View File

@ -32,7 +32,6 @@
<string name="button_save">Spremi</string>
<string name="pref_general_title">Opće</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="tab_tracks">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_poi_removed">Oznaka točke interesa uklonjena.</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="descr_button_location">Centriraj na trenutačno mjesto</string>
</resources>

View File

@ -182,7 +182,6 @@
<string name="dialog_generic_details_button">Tampilkan detil</string>
<string name="dialog_generic_button_okay">Oke</string>
<string name="dialog_generic_button_cancel">Batalkan</string>
<string name="button_resume">Lanjutkan</string>
<string name="button_save">Simpan</string>
<string name="button_delete">Bersihkan</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_resume">Lanjutkan</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>
</resources>

View File

@ -21,7 +21,6 @@
<!-- FAB Sub Menu -->
<string name="button_delete">Cancella</string>
<string name="button_save">Salva</string>
<string name="button_resume">Riprendi</string>
<!-- Dialogs -->
<string name="dialog_generic_button_cancel">Annulla</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_name" translatable="false">July 20, 1969</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>
</resources>

View File

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

View File

@ -18,7 +18,6 @@
<!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Tøm</string>
<string name="button_save">Lagre og tøm</string>
<string name="button_resume">Fortsett</string>
<!-- dialogs -->
<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>
@ -109,9 +108,6 @@
<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_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="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>
</resources>

View File

@ -18,7 +18,6 @@
<!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Wissen</string>
<string name="button_save">Opslaan</string>
<string name="button_resume">Hervatten</string>
<!-- dialogs -->
<string name="dialog_share_gpx">GPX-bestand delen met</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_poi_removed">POI-indicatie verwijderd.</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="descr_button_location">Centreren op huidige locatie</string>
</resources>

View File

@ -3,7 +3,6 @@
<string name="dialog_generic_details_button">Pokaż szczegóły</string>
<string name="dialog_generic_button_okay">OK</string>
<string name="dialog_generic_button_cancel">Anuluj</string>
<string name="button_resume">Kontynuuj</string>
<string name="button_save">Zapisz</string>
<string name="button_delete">Wyczyść</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_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_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_map_current_track">Odwzorowanie bieżącego toru</string>
<string name="abbreviation_minutes">min</string>

View File

@ -3,7 +3,6 @@
<!-- App Name -->
<string name="app_name">Trackbook</string>
<!-- please do not translate app_name - transcription into different alphabet types is fine though -->
<string name="app_version_name" translatable="false">\"Echoes\"</string>
<!-- Tabs -->
<string name="tab_map">Mapa</string>
<string name="tab_tracks">Caminhos</string>
@ -22,7 +21,6 @@
<!-- FAB Sub Menu -->
<string name="button_delete">Limpar</string>
<string name="button_save">Salvar</string>
<string name="button_resume">Retomar</string>
<!-- Dialogs -->
<string name="dialog_generic_button_cancel">Cancelar</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_edit_button">Botão editar rota</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="pref_recording_accuracy_summary_default">Os waypoints têm menor precisão, mas são mais freqüentes.</string>
</resources>

View File

@ -33,7 +33,6 @@
<string name="dialog_generic_details_button">Показать подробности</string>
<string name="dialog_generic_button_okay">ОЕ</string>
<string name="dialog_generic_button_cancel">Отмена</string>
<string name="button_resume">Продолжить</string>
<string name="button_save">Сохранить</string>
<string name="button_delete">Очистить</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_imperial_measurement_units_summary_metric">В настоящее время используются метрические единицы (километр, метр).</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_report_issue_title">Выпуск отчета</string>
<string name="pref_theme_selection_mode_dark">Темный режим</string>
@ -81,7 +78,6 @@
<string name="abbreviation_seconds">сек</string>
<string name="abbreviation_minutes">мин</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_summary_imperial">В настоящее время используются имперские единицы (мили, футы).</string>
<string name="pref_gps_only_title">Ограничение на GPS</string>

View File

@ -19,7 +19,6 @@
<!-- fab sub menu_bottom_navigation -->
<string name="button_delete">Rensa</string>
<string name="button_save">Spara</string>
<string name="button_resume">Återuppta</string>
<!-- dialogs -->
<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>
@ -96,7 +95,6 @@
<string name="descr_statistics_sheet_edit_button">Spårredigeringsknapp</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="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_advanced_title">Avancerad</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_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_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_1">Dina inspelade spår</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_mark_starred_button">Markera som stjärnmärkt</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="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>

View File

@ -8,7 +8,6 @@
<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="notification_resume">Devam ettir</string>
<string name="button_resume">Devam ettir</string>
<string name="descr_button_delete">Kaydı temizle</string>
<string name="descr_button_save">Kaydı kaydet</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_map">Harita</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="descr_button_location">Geçerli konuma ortala</string>
</resources>

View File

@ -21,7 +21,6 @@
<!-- Buttons -->
<string name="button_delete">删除</string>
<string name="button_pause">暂停</string>
<string name="button_resume">恢复</string>
<string name="button_save">保存</string>
<string name="button_start">开始</string>
<!-- Dialogs -->
@ -92,9 +91,6 @@
<string name="pref_imperial_measurement_units_summary_metric">当前正使用公制单位(千米,米)。</string>
<string name="pref_imperial_measurement_units_summary_imperial">当前正使用英制单位(英里,英尺)。</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_title">报告问题</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_dark">#FFCA2D23</color>
<color name="default_blue">#FF3C98DB</color>
<color name="homepoint">#FFFFC107</color>
<color name="default_green">#FF4CAF50</color>
</resources>

View File

@ -1,18 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 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 -->
<string name="app_version_name" translatable="false">\"See Emily Play\"</string>
<!-- Tabs -->
<string name="tab_map">Map</string>
<string name="tab_tracks">Tracks</string>
<string name="tab_settings">Settings</string>
<!-- Notification -->
<string name="notification_title_trackbook_running">Trackbook running</string>
<string name="notification_title_trackbook_not_running">Trackbook not running</string>
<string name="notification_pause">Pause</string>
<string name="notification_resume">Resume</string>
<string name="notification_title_trackbook_not_running">Trackbook stopped</string>
<string name="notification_pause">Stop</string>
<string name="notification_resume">Record</string>
<string name="notification_show">Show</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>
@ -21,10 +20,9 @@
<string name="snackbar_message_location_permission_denied">Location permission not granted. Trackbook will not work.</string>
<!-- Buttons -->
<string name="button_delete">Delete</string>
<string name="button_pause">Pause</string>
<string name="button_resume">Resume</string>
<string name="button_pause">Stop</string>
<string name="button_save">Save</string>
<string name="button_start">Start</string>
<string name="button_start">Record</string>
<!-- Dialogs -->
<string name="dialog_delete_current_recording_message">Discard current recording?</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_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_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_device_id">Device ID</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_title">Delete Non-Starred Recordings</string>
@ -117,7 +117,7 @@
<!-- Descriptions -->
<string name="descr_button_delete">Discard recording</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_save">Save recording</string>
<string name="descr_button_start">Start recording</string>