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 = mutableListOf() -): Parcelable -{ - fun delete_non_starred(context: Context) - { - val to_delete: List = 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().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))} -} diff --git a/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt b/app/src/main/java/org/y20k/trackbook/core/Trkpt.kt similarity index 70% rename from app/src/main/java/org/y20k/trackbook/core/WayPoint.kt rename to app/src/main/java/org/y20k/trackbook/core/Trkpt.kt index dcf8366..7ed4b9c 100644 --- a/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt +++ b/app/src/main/java/org/y20k/trackbook/core/Trkpt.kt @@ -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 } diff --git a/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt index 7d19ccb..adafc83 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt @@ -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 -> diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt index 161eefe..66c358b 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt @@ -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 diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt index cee8481..6206004 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt @@ -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 { - // 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 diff --git a/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt index f145c66..eb6bb67 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt @@ -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 { - + fun createMyLocationOverlay(context: Context, location: Location, trackingState: Int): ItemizedIconOverlay + { val overlayItems: ArrayList = ArrayList() - 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 + { + val overlayItems: ArrayList = ArrayList() + + // 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 = 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 { + fun createSpecialMakersTrackOverlay(context: Context, track: Track, trackingState: Int, displayStartEndMarker: Boolean = false): ItemizedIconOverlay + { val overlayItems: ArrayList = ArrayList() 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, enableStarring: Boolean): ItemizedIconOverlay { return ItemizedIconOverlay(context, overlayItems, diff --git a/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt index fe80391..8a15e15 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt @@ -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, diff --git a/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt index dec7997..47d7980 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt @@ -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 diff --git a/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt index 9925584..9f8f6c2 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt @@ -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) { diff --git a/app/src/main/java/org/y20k/trackbook/helpers/functions.kt b/app/src/main/java/org/y20k/trackbook/helpers/functions.kt new file mode 100644 index 0000000..b738802 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/functions.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt b/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt index 7a4cf72..12f5774 100644 --- a/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt +++ b/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt @@ -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() { - - /* Define log tag */ - private val TAG: String = LogHelper.makeLogTag(TracklistAdapter::class.java) - - +class TracklistAdapter(val fragment: Fragment, val database: Database) : RecyclerView.Adapter() +{ /* 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 = ArrayList() /* 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 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 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> = ArrayList() private var currentPositionOverlay: ItemizedIconOverlay private var currentTrackOverlay: SimpleFastPointOverlay? private var currentTrackSpecialMarkerOverlay: ItemizedIconOverlay? - 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 } } diff --git a/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt b/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt index f244897..f6d2d11 100644 --- a/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt +++ b/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt @@ -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 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 } } diff --git a/app/src/main/res/drawable/ic_fiber_manual_stop_24dp.xml b/app/src/main/res/drawable/ic_fiber_manual_stop_24dp.xml new file mode 100644 index 0000000..d979a04 --- /dev/null +++ b/app/src/main/res/drawable/ic_fiber_manual_stop_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_homepoint_24dp.xml b/app/src/main/res/drawable/ic_homepoint_24dp.xml new file mode 100644 index 0000000..836c9cb --- /dev/null +++ b/app/src/main/res/drawable/ic_homepoint_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/element_track.xml b/app/src/main/res/layout/element_track.xml index 3bae159..ac2d419 100644 --- a/app/src/main/res/layout/element_track.xml +++ b/app/src/main/res/layout/element_track.xml @@ -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" /> - - diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index ed9c17e..df94f88 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -22,40 +22,11 @@ app:layout_dodgeInsetEdges="bottom"> - - + - - - - - - - - - - diff --git a/app/src/main/res/layout/track_statistics.xml b/app/src/main/res/layout/track_statistics.xml index 0e1b2a3..793dd01 100755 --- a/app/src/main/res/layout/track_statistics.xml +++ b/app/src/main/res/layout/track_statistics.xml @@ -74,30 +74,6 @@ app:layout_constraintTop_toTopOf="@+id/statistics_track_name_headline" app:srcCompat="@drawable/ic_save_to_storage_24dp" /> - - - - + app:layout_constraintTop_toBottomOf="@+id/statistics_track_name_headline" /> Ryd Gem - Fortsæt Del GPX fil med Trackbook har ingen rutepunkter endnu. @@ -86,9 +85,6 @@ Om Samlet registreret distance Rapporter fejl og foreslå forbedringer på GitHub. - Nøjagtighed af optagelser - Waypoints har lavere nøjagtighed, men er mere hyppige. - Waypoints har større nøjagtighed, men er mindre hyppige. I øjeblikket anvendes metriske enheder (kilometer, meter). I øjeblikket bruger vi kun GPS til lokalisering. I øjeblikket bruges GPS og netværk til lokalisering. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2f302b7..a3b5ce7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -18,7 +18,6 @@ Zurücksetzen Speichern - Fortsetzen GPX-Datei teilen mit Trackbook hat noch keine Wegpunkte aufgenommen. @@ -110,7 +109,4 @@ Markierung für Ort von Interesse entfernt. Markierung für Ort von Interesse hinzugefügt. Erfasste Gesamtentfernung - Wegpunkte haben eine höhere Genauigkeit, sind aber weniger häufig. - Wegpunkte haben eine geringere Genauigkeit, sind aber häufiger. - Aufzeichnungsgenauigkeit \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1cf3ed0..15503cf 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -28,7 +28,6 @@ Permiso de ubicación no concedido. Trackbook no funcionará. Limpiar Guardar - Resumir OK Mostrar detalles Trackbook no registró ningún punto de referencia hasta el momento. @@ -77,9 +76,6 @@ Actualmente usando GPS y Red para localización. Actualmente se utilizan unidades métricas (Kilómetro, Metro). Utilizar medidas imperiales - Los puntos de referencia tienen mayor precisión pero son menos frecuentes. - Los waypoints tienen menor precisión pero son más frecuentes. - Precisión de grabación Reportar problema Restablece la configuración avanzada a los valores predeterminados. Reiniciar diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a1a5964..bc28ee6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -17,7 +17,6 @@ Localisation désactivée. Le suivi ne fonctionnera pas. Supprimer - Reprendre Sauvegarder Reprendre l\'enregistrement @@ -109,9 +108,6 @@ Copié dans le presse-papiers. Marqueur de point d\'intérêt supprimé. Marqueur de point d\'intérêt ajouté. - Précision de l\'enregistrement - Les points de cheminements ont une précision plus faible mais sont plus fréquents. - Les points de cheminement ont une plus grande précision mais sont moins fréquents. Distance totale enregistrée Centrer sur la position actuelle \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 82bdeef..5da3568 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -32,7 +32,6 @@ Spremi Opće Prag točnosti - Nastavi Tema programa Rute Mapa zadnje rute @@ -96,9 +95,6 @@ Kopirano u međuspremnik. Oznaka točke interesa uklonjena. Oznaka točke interesa dodana. - Točke rute imaju veću točnost ali su rjeđe. - Točnost snimanja - Točke rute imaju manju točnost ali su češće. Ukupna udaljenost snimljena Centriraj na trenutačno mjesto \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index cf94933..54f7678 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -182,7 +182,6 @@ Tampilkan detil Oke Batalkan - Lanjutkan Simpan Bersihkan Izin lokasi tak diberikan. Trackbook tidak akan berfungsi. @@ -192,8 +191,5 @@ Tampilkan Lanjutkan Pengaturan - Waypoints memiliki akurasi yang lebih rendah tetapi lebih sering. - Waypoints memiliki akurasi yang lebih tinggi tetapi lebih jarang. - Akurasi Perekaman Jarak Total Terekam \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5a85c36..8d355e1 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -21,7 +21,6 @@ Cancella Salva - Riprendi Annulla OK @@ -116,8 +115,5 @@ 23.0 km • 5 hrs 23 min 42 sec July 20, 1969 track data missing - Precisione di registrazione - I waypoint hanno una maggiore precisione ma sono meno frequenti. - I waypoint hanno una precisione inferiore ma sono più frequenti. Distanza totale registrata \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 4933068..8188d3b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -18,7 +18,6 @@ クリア 保存してクリア - 再開 GPX ファイルを共有... トラックブックはこれまでウェイポイントを記録していません。 diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 92335aa..6d4b39f 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -18,7 +18,6 @@ Tøm Lagre og tøm - Fortsett Del GPX-fil med Trackbook har ikke registrert noen veipunkter så langt. @@ -109,9 +108,6 @@ Interessepunktmarkør lagt til. Interessepunktmarkør fjernet. Kopiert til utklippstavle. - Veipunkter har høyere nøyaktighet, men er mindre hyppige. Totalavstand registrert - Veipunkter har lavere nøyaktighet, men er hyppigere. - Opptaksnøyaktighet Sentrer på nåværende sted \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b7a66c2..ac15150 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -18,7 +18,6 @@ Wissen Opslaan - Hervatten GPX-bestand delen met Trackbook heeft nog geen routepunten vastgelegd. @@ -109,9 +108,6 @@ Gekopieerd naar klembord. POI-indicatie verwijderd. POI-indicatie toegevoegd. - Opname nauwkeurigheid - Routepunten zijn minder nauwkeurig, maar frequenter. - Routepunten zijn nauwkeuriger, maar minder frequent. Totaal afgelegde afstand Centreren op huidige locatie \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 9b41a99..090f55d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -3,7 +3,6 @@ Pokaż szczegóły OK Anuluj - Kontynuuj Zapisz Wyczyść Lokalizacja jest wyłączona. Trackbook nie będzie działać. @@ -76,9 +75,6 @@ Resetuj Przywróć ustawienia zaawansowane do wartości domyślnych. Zgłaszaj błędy i proponuj ulepszenia na GitHub. - Dokładność zapisu - Punkty trasy mają mniejszą dokładność, ale są częstsze. - Punkty trasy mają większą dokładność, ale występują rzadziej. Przycisk Wznów Odwzorowanie bieżącego toru min diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index c3eee3e..fd74e93 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -3,7 +3,6 @@ Trackbook - \"Echoes\" Mapa Caminhos @@ -22,7 +21,6 @@ Limpar Salvar - Retomar Cancelar OK @@ -113,8 +111,5 @@ Botão apagar rota Botão editar rota Botão salvar como GPX - Os waypoints têm maior precisão, mas são menos freqüentes. - Precisão de Gravação Distância Total Registrada - Os waypoints têm menor precisão, mas são mais freqüentes. \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 7378cdb..752f6da 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -33,7 +33,6 @@ Показать подробности ОЕ Отмена - Продолжить Сохранить Очистить Разрешение на определение местоположения не предоставлено. Trackbook работать не будет. @@ -67,8 +66,6 @@ В настоящее время для локализации используется GPS и сеть. В настоящее время используются метрические единицы (километр, метр). В настоящее время для локализации используется только GPS. - Точность записи - Путевые точки имеют более низкую точность, но встречаются чаще. Сброс расширенных настроек до значений по умолчанию. Выпуск отчета Темный режим @@ -81,7 +78,6 @@ сек мин Сообщайте об ошибках и предлагайте улучшения на GitHub. - Путевые точки имеют более высокую точность, но встречаются реже. Используйте имперские меры В настоящее время используются имперские единицы (мили, футы). Ограничение на GPS diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 840ced0..7cbe535 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -19,7 +19,6 @@ Rensa Spara - Återuppta Dela GPX-fil med Trackbook spelade inte in några vägpunkter så här långt. @@ -96,7 +95,6 @@ Spårredigeringsknapp Återställ Tips: Noggrannheten hos höjddata beror på din enhet. Höjden i upp- och nedförsbacke för hela rutten mäts. - Vägpunkter har högre noggrannhet men är mindre frekventa. För närvarande används metriska enheter (kilometer, meter). Avancerad Tröskel för noggrannhet @@ -115,7 +113,6 @@ Återställ avancerade inställningar till standardinställningarna. Rapportera frågan Rapportera fel och föreslå förbättringar på GitHub. - Noggrannhet vid inspelning ... kommer att visas här. Dina inspelade spår Ikon för Trackbook-appen @@ -156,7 +153,6 @@ Stoppa inspelningen Markera som stjärnmärkt Återuppta inspelning - Vägpunkter har lägre noggrannhet men är mer frekventa. Total varaktighet: För närvarande används endast GPS för lokalisering. Ta bort alla inspelningar i \"Tracks\" som inte har stjärnor. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1c51a0c..a3e51db 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -8,7 +8,6 @@ Yıldızlı olarak işaretle düğmesi Kaydı devam ettir Devam ettir - Devam ettir Kaydı temizle Kaydı kaydet Kaydı başlat düğmesi @@ -96,9 +95,6 @@ Yollar Harita Trackbook - Kayıt Doğruluğu - Ara noktalar daha düşük doğruluğa sahiptir ancak daha sıktır. - Ara noktalar daha yüksek doğruluğa sahiptir ancak daha az sıklıktadır. Kaydedilen Toplam Mesafe Geçerli konuma ortala \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 088a5cf..f4de7d3 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -21,7 +21,6 @@ 删除 暂停 - 恢复 保存 开始 @@ -92,9 +91,6 @@ 当前正使用公制单位(千米,米)。 当前正使用英制单位(英里,英尺)。 使用英制测量 - 航点的精确度较高,但频率较低。 - 航点的精确度较低,但频率较高。 - 记录精确度 在 Github 上报告漏洞并提出改进建议。 报告问题 重置高级设置为默认值。 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1587c46..4a65370 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -47,6 +47,7 @@ #FFDC3D33 #FFCA2D23 #FF3C98DB + #FFFFC107 #FF4CAF50 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2b610e8..2fce762 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,18 +1,17 @@ - Trackbook-v + trkpt - \"See Emily Play\" Map Tracks Settings Trackbook running - Trackbook not running - Pause - Resume + Trackbook stopped + Stop + Record Show Movement Recording State Display duration and distance. Option to pause movement recording. @@ -21,10 +20,9 @@ Location permission not granted. Trackbook will not work. Delete - Pause - Resume + Stop Save - Start + Record Discard current recording? Discard @@ -86,7 +84,9 @@ Number of waypoints used to smooth the elevation curve. Altitude Smoothing Automatically export GPX file after this many hours. + A unique ID to distinguish tracks recorded across multiple devices. Auto Export Interval + Device ID Advanced Delete all recordings in \"Tracks\" that are not starred. Delete Non-Starred Recordings @@ -117,7 +117,7 @@ Discard recording Center on current location - Pause recording + Stop recording Resume recording Save recording Start recording