diff --git a/app/build.gradle b/app/build.gradle
index f576d0d..228a573 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1df00a4..990823e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,7 +22,7 @@
- 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
*/
-
}
diff --git a/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt b/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt
index 4616c8a..224b69f 100644
--- a/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt
+++ b/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt
@@ -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)
- }
-
-
}
+
diff --git a/app/src/main/java/org/y20k/trackbook/TrackFragment.kt b/app/src/main/java/org/y20k/trackbook/TrackFragment.kt
index 6c16f8c..393ea5d 100644
--- a/app/src/main/java/org/y20k/trackbook/TrackFragment.kt
+++ b/app/src/main/java/org/y20k/trackbook/TrackFragment.kt
@@ -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()
- }
- }
-
}
diff --git a/app/src/main/java/org/y20k/trackbook/Trackbook.kt b/app/src/main/java/org/y20k/trackbook/Trackbook.kt
index 3185c28..b90dfa5 100644
--- a/app/src/main/java/org/y20k/trackbook/Trackbook.kt
+++ b/app/src/main/java/org/y20k/trackbook/Trackbook.kt
@@ -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 = 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
+ {
+ 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()
}
-
}
\ No newline at end of file
diff --git a/app/src/main/java/org/y20k/trackbook/TrackerService.kt b/app/src/main/java/org/y20k/trackbook/TrackerService.kt
index 62979ba..1b0ee32 100644
--- a/app/src/main/java/org/y20k/trackbook/TrackerService.kt
+++ b/app/src/main/java/org/y20k/trackbook/TrackerService.kt
@@ -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()
- {
- 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
- }
- }
}
diff --git a/app/src/main/java/org/y20k/trackbook/TrackingToggleTileService.kt b/app/src/main/java/org/y20k/trackbook/TrackingToggleTileService.kt
index f50d60d..773d1dc 100644
--- a/app/src/main/java/org/y20k/trackbook/TrackingToggleTileService.kt
+++ b/app/src/main/java/org/y20k/trackbook/TrackingToggleTileService.kt
@@ -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
diff --git a/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt b/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt
index 2f2ec3f..f6cce15 100644
--- a/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt
+++ b/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt
@@ -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
}
diff --git a/app/src/main/java/org/y20k/trackbook/core/Database.kt b/app/src/main/java/org/y20k/trackbook/core/Database.kt
new file mode 100644
index 0000000..f9d3997
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/core/Database.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/y20k/trackbook/core/Homepoint.kt b/app/src/main/java/org/y20k/trackbook/core/Homepoint.kt
new file mode 100644
index 0000000..1acef79
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/core/Homepoint.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/y20k/trackbook/core/Track.kt b/app/src/main/java/org/y20k/trackbook/core/Track.kt
index 8917fce..4ea965c 100644
--- a/app/src/main/java/org/y20k/trackbook/core/Track.kt
+++ b/app/src/main/java/org/y20k/trackbook/core/Track.kt
@@ -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 = mutableListOf(),
- @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 = ArrayDeque(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("""
""".trimIndent())
- gpxString.appendLine("\t")
- gpxString.appendLine("\t\tTrackbook Recording: ${this.name}")
- gpxString.appendLine("\t")
-
- // POIs
- val poiList: List = this.wayPoints.filter { it.starred }
- poiList.forEach { poi ->
- gpxString.appendLine("\t")
- gpxString.appendLine("\t\tPoint of interest")
- gpxString.appendLine("\t\t${poi.altitude}")
- gpxString.appendLine("\t")
- }
+ write("\t")
+ write("\t\tTrackbook Recording: ${this.name}")
+ write("\t\t${this.device_id}")
+ write("\t")
// TRK
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
- gpxString.appendLine("\t")
- gpxString.appendLine("\t\t${this.name}")
- gpxString.appendLine("\t\t")
- this.wayPoints.forEach { wayPoint ->
- gpxString.appendLine("\t\t\t")
- gpxString.appendLine("\t\t\t\t${wayPoint.altitude}")
- gpxString.appendLine("\t\t\t\t")
- gpxString.appendLine("\t\t\t\t${wayPoint.numberSatellites}")
- gpxString.appendLine("\t\t\t")
+ write("\t")
+ write("\t\t${this.name}")
+ write("\t\t")
+
+ trkpt_generator().forEach { trkpt ->
+ write("\t\t\t")
+ write("\t\t\t\t${trkpt.altitude}")
+ write("\t\t\t\t")
+ write("\t\t\t\t${trkpt.numberSatellites}")
+ write("\t\t\t")
}
- gpxString.appendLine("\t\t")
- gpxString.appendLine("\t")
- gpxString.appendLine("")
- return gpxString.toString()
+ write("\t\t")
+ write("\t")
+ write("")
+
+ 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
+ {
+ 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,
+)
diff --git a/app/src/main/java/org/y20k/trackbook/core/Tracklist.kt b/app/src/main/java/org/y20k/trackbook/core/Tracklist.kt
deleted file mode 100644
index 1e07b62..0000000
--- a/app/src/main/java/org/y20k/trackbook/core/Tracklist.kt
+++ /dev/null
@@ -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