Refactor out TracklistElement, make Track more object-oriented.
I'm sure the reason for TracklistElement was to save memory by not having the waypoints loaded, but the separation of Track / TracklistElement creates lots of back-and-forth conversions and mental overhead. I want to give this a try and see if it actually causes any problems. Moved a lot of helper functions into the classes they operate on.
This commit is contained in:
		
							parent
							
								
									45de00b9c5
								
							
						
					
					
						commit
						e3bc911de4
					
				
					 12 changed files with 555 additions and 791 deletions
				
			
		|  | @ -28,6 +28,7 @@ import android.view.View | |||
| import android.view.ViewGroup | ||||
| import androidx.activity.result.contract.ActivityResultContracts.RequestPermission | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.core.net.toUri | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.navigation.fragment.findNavController | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
|  | @ -36,11 +37,9 @@ import kotlinx.coroutines.Dispatchers.Main | |||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import org.y20k.trackbook.core.Track | ||||
| import org.y20k.trackbook.core.TracklistElement | ||||
| import org.y20k.trackbook.helpers.* | ||||
| import org.y20k.trackbook.ui.MapFragmentLayoutHolder | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
|  * MapFragment class | ||||
|  */ | ||||
|  | @ -49,7 +48,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe | |||
|     /* Define log tag */ | ||||
|     private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java) | ||||
| 
 | ||||
| 
 | ||||
|     /* Main class variables */ | ||||
|     private var bound: Boolean = false | ||||
|     private val handler: Handler = Handler(Looper.getMainLooper()) | ||||
|  | @ -217,7 +215,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe | |||
|     override fun onMarkerTapped(latitude: Double, longitude: Double) { | ||||
|         super.onMarkerTapped(latitude, longitude) | ||||
|         if (bound) { | ||||
|             track = TrackHelper.toggleStarred(activity as Context, track, latitude, longitude) | ||||
|             TrackHelper.toggle_waypoint_starred(activity as Context, track, latitude, longitude) | ||||
|             layout.overlayCurrentTrack(track, trackingState) | ||||
|             trackerService.track = track | ||||
|         } | ||||
|  | @ -298,16 +296,12 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe | |||
|         else | ||||
|         { | ||||
|             CoroutineScope(IO).launch { | ||||
|                 // step 1: create and store filenames for json and gpx files | ||||
|                 track.trackUriString = FileHelper.getTrackFileUri(activity as Context, track).toString() | ||||
|                 track.gpxUriString = FileHelper.getGpxFileUri(activity as Context, track).toString() | ||||
|                 // step 2: save track | ||||
|                 FileHelper.saveTrackSuspended(track, saveGpxToo = true) | ||||
|                 // step 3: clear track | ||||
|                 track.save_json(activity as Context) | ||||
|                 track.save_gpx(activity as Context) | ||||
|                 trackerService.clearTrack() | ||||
|                 // step 4: open track in TrackFragement | ||||
|                 withContext(Main) { | ||||
|                     openTrack(track.toTracklistElement(activity as Context)) | ||||
|                     // step 4: open track in TrackFragement | ||||
|                     openTrack(track) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | @ -315,12 +309,12 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe | |||
| 
 | ||||
| 
 | ||||
|     /* Opens a track in TrackFragment */ | ||||
|     private fun openTrack(tracklistElement: TracklistElement) { | ||||
|     private fun openTrack(track: Track) { | ||||
|         val bundle: Bundle = Bundle() | ||||
|         bundle.putString(Keys.ARG_TRACK_TITLE, tracklistElement.name) | ||||
|         bundle.putString(Keys.ARG_TRACK_FILE_URI, tracklistElement.trackUriString) | ||||
|         bundle.putString(Keys.ARG_GPX_FILE_URI, tracklistElement.gpxUriString) | ||||
|         bundle.putLong(Keys.ARG_TRACK_ID, tracklistElement.id) | ||||
|         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) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ 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 | ||||
|  | @ -216,12 +217,8 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList | |||
| 
 | ||||
|     /* Removes track and track files for given position - used by TracklistFragment */ | ||||
|     private fun deleteNonStarred(context: Context) { | ||||
|         CoroutineScope(IO).launch  { | ||||
|             var tracklist: Tracklist = FileHelper.readTracklist(context) | ||||
|             val deferred: Deferred<Tracklist> = async { FileHelper.deleteNonStarredSuspended(context, tracklist) } | ||||
|             // wait for result and store in tracklist | ||||
|             tracklist = deferred.await() | ||||
|         } | ||||
|         var tracklist: Tracklist = load_tracklist(context) | ||||
|         tracklist.delete_non_starred(context) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ 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 | ||||
|  | @ -40,7 +41,9 @@ import kotlinx.coroutines.CoroutineScope | |||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| 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 | ||||
|  | @ -60,7 +63,8 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
| 
 | ||||
| 
 | ||||
|     /* Overrides onCreate from Fragment */ | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|     override fun onCreate(savedInstanceState: Bundle?) | ||||
|     { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         trackFileUriString = arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String() | ||||
|     } | ||||
|  | @ -70,8 +74,9 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
|     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { | ||||
|         // initialize layout | ||||
|         val track: Track | ||||
|         if (this::trackFileUriString.isInitialized && trackFileUriString.isNotBlank()) { | ||||
|             track = FileHelper.readTrack(activity as Context, Uri.parse(trackFileUriString)) | ||||
|         if (this::trackFileUriString.isInitialized && trackFileUriString.isNotBlank()) | ||||
|         { | ||||
|             track = track_from_file(activity as Context, Uri.parse(trackFileUriString).toFile()) | ||||
|         } else { | ||||
|             track = Track() | ||||
|         } | ||||
|  | @ -107,13 +112,15 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
| 
 | ||||
| 
 | ||||
|     /* Overrides onResume from Fragment */ | ||||
|     override fun onResume() { | ||||
|     override fun onResume() | ||||
|     { | ||||
|         super.onResume() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Overrides onPause from Fragment */ | ||||
|     override fun onPause() { | ||||
|     override fun onPause() | ||||
|     { | ||||
|         super.onPause() | ||||
|         // save zoom level and map center | ||||
|         layout.saveViewStateToTrack() | ||||
|  | @ -125,15 +132,18 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
| 
 | ||||
| 
 | ||||
|     /* Pass the activity result */ | ||||
|     private fun requestSaveGpxResult(result: ActivityResult) { | ||||
|     private fun requestSaveGpxResult(result: ActivityResult) | ||||
|     { | ||||
|         // save GPX file to result file location | ||||
|         if (result.resultCode == Activity.RESULT_OK && result.data != null) { | ||||
|             val sourceUri: Uri = Uri.parse(layout.track.gpxUriString) | ||||
|         if (result.resultCode == Activity.RESULT_OK && result.data != null) | ||||
|         { | ||||
|             val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri() | ||||
|             val targetUri: Uri? = result.data?.data | ||||
|             if (targetUri != null) { | ||||
|             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) | ||||
|                     FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri) | ||||
|                 } | ||||
|                 Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show() | ||||
|             } | ||||
|  | @ -142,7 +152,8 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
| 
 | ||||
| 
 | ||||
|     /* Overrides onRenameTrackDialog from RenameTrackDialog */ | ||||
|     override fun onRenameTrackDialog(textInput: String) { | ||||
|     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 | ||||
|  | @ -152,10 +163,13 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
| 
 | ||||
| 
 | ||||
|     /* Overrides onYesNoDialog from YesNoDialogListener */ | ||||
|     override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) { | ||||
|         when (type) { | ||||
|     override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) | ||||
|     { | ||||
|         when (type) | ||||
|         { | ||||
|             Keys.DIALOG_DELETE_TRACK -> { | ||||
|                 when (dialogResult) { | ||||
|                 when (dialogResult) | ||||
|                 { | ||||
|                     // user tapped remove track | ||||
|                     true -> { | ||||
|                         // switch to TracklistFragment and remove track there | ||||
|  | @ -169,25 +183,31 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
| 
 | ||||
| 
 | ||||
|     /* Overrides onMarkerTapped from MarkerListener */ | ||||
|     override fun onMarkerTapped(latitude: Double, longitude: Double) { | ||||
|     override fun onMarkerTapped(latitude: Double, longitude: Double) | ||||
|     { | ||||
|         super.onMarkerTapped(latitude, longitude) | ||||
|         // update track display | ||||
|         layout.track = TrackHelper.toggleStarred(activity as Context, layout.track, latitude, longitude) | ||||
|         TrackHelper.toggle_waypoint_starred(activity as Context, layout.track, latitude, longitude) | ||||
|         layout.updateTrackOverlay() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Opens up a file picker to select the save location */ | ||||
|     private fun openSaveGpxDialog() { | ||||
|     private fun openSaveGpxDialog() | ||||
|     { | ||||
|         val context = this.activity as Context | ||||
|         val export_name: String = DateTimeHelper.convertToSortableDateString(layout.track.recordingStart) + Keys.GPX_FILE_EXTENSION | ||||
|         val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { | ||||
|             addCategory(Intent.CATEGORY_OPENABLE) | ||||
|             type = Keys.MIME_TYPE_GPX | ||||
|             putExtra(Intent.EXTRA_TITLE, FileHelper.getGpxFileName(layout.track)) | ||||
|             putExtra(Intent.EXTRA_TITLE, export_name) | ||||
|         } | ||||
|         // file gets saved in the ActivityResult | ||||
|         try { | ||||
|         try | ||||
|         { | ||||
|             requestSaveGpxLauncher.launch(intent) | ||||
|         } catch (e: Exception) { | ||||
|         } | ||||
|         catch (e: Exception) | ||||
|         { | ||||
|             LogHelper.e(TAG, "Unable to save GPX.") | ||||
|             Toast.makeText(activity as Context, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show() | ||||
|         } | ||||
|  | @ -195,8 +215,9 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
| 
 | ||||
| 
 | ||||
|     /* Share track as GPX via share sheet */ | ||||
|     private fun shareGpxTrack() { | ||||
|         val gpxFile = Uri.parse(layout.track.gpxUriString).toFile() | ||||
|     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 | ||||
|  | @ -208,9 +229,12 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
| 
 | ||||
|         // show share sheet - if file helper is available | ||||
|         val packageManager: PackageManager? = activity?.packageManager | ||||
|         if (packageManager != null && shareIntent.resolveActivity(packageManager) != null) { | ||||
|         if (packageManager != null && shareIntent.resolveActivity(packageManager) != null) | ||||
|         { | ||||
|             startActivity(shareIntent) | ||||
|         } else { | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             Toast.makeText(activity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show() | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -41,6 +41,7 @@ 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.helpers.* | ||||
| 
 | ||||
| /* | ||||
|  | @ -61,6 +62,8 @@ class TrackerService: Service(), SensorEventListener { | |||
|     var currentBestLocation: Location = LocationHelper.getDefaultLocation() | ||||
|     var lastSave: Date = Keys.DEFAULT_DATE | ||||
|     var stepCountOffset: Float = 0f | ||||
|     // The resumed flag will be true for the first point that is received after unpausing a | ||||
|     // recording, so that the distance travelled while paused is not added to the track.distance. | ||||
|     var resumed: Boolean = false | ||||
|     var track: Track = Track() | ||||
|     var gpsLocationListenerRegistered: Boolean = false | ||||
|  | @ -93,7 +96,7 @@ class TrackerService: Service(), SensorEventListener { | |||
|         networkLocationListener = createLocationListener() | ||||
|         trackingState = PreferencesHelper.loadTrackingState() | ||||
|         currentBestLocation = LocationHelper.getLastKnownLocation(this) | ||||
|         track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this)) | ||||
|         track = load_temp_track(this) | ||||
| //        altitudeValues.capacity = PreferencesHelper.loadAltitudeSmoothingValue() | ||||
|         PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener) | ||||
|     } | ||||
|  | @ -195,72 +198,68 @@ class TrackerService: Service(), SensorEventListener { | |||
| 
 | ||||
|     /* Resume tracking after stop/pause */ | ||||
|     fun resumeTracking() { | ||||
|         // load temp track - returns an empty track if not available | ||||
|         track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this)) | ||||
|         // load temp track - returns an empty track if there is no temp file. | ||||
|         track = load_temp_track(this) | ||||
|         // try to mark last waypoint as stopover | ||||
|         if (track.wayPoints.size > 0) { | ||||
|             val lastWayPointIndex = track.wayPoints.size - 1 | ||||
|             track.wayPoints[lastWayPointIndex].isStopOver = true | ||||
|         } | ||||
|         // set resumed flag | ||||
|         resumed = true | ||||
|         // calculate length of recording break | ||||
|         track.recordingPaused = track.recordingPaused + TrackHelper.calculateDurationOfPause(track.recordingStop) | ||||
|         // start tracking | ||||
|         track.recordingPaused += TrackHelper.calculateDurationOfPause(track.recordingStop) | ||||
|         startTracking(newTrack = false) | ||||
|     } | ||||
| 
 | ||||
|     /* Start tracking location */ | ||||
|     fun startTracking(newTrack: Boolean = true) { | ||||
|         // start receiving location updates | ||||
|         addGpsLocationListener() | ||||
|         addNetworkLocationListener() | ||||
|         // set up new track | ||||
|         if (newTrack) { | ||||
|             track = Track() | ||||
|             track.name = DateTimeHelper.convertToReadableDate(track.recordingStart) | ||||
|             resumed = false | ||||
|             stepCountOffset = 0f | ||||
|         } | ||||
|         // set state | ||||
|         trackingState = Keys.STATE_TRACKING_ACTIVE | ||||
|         PreferencesHelper.saveTrackingState(trackingState) | ||||
|         // start recording steps and location fixes | ||||
|         startStepCounter() | ||||
|         handler.postDelayed(periodicTrackUpdate, 0) | ||||
|         // show notification | ||||
|         startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification()) | ||||
|     } | ||||
| 
 | ||||
|     /* Stop tracking location */ | ||||
|     fun stopTracking() { | ||||
|         // save temp track | ||||
|         track.recordingStop = GregorianCalendar.getInstance().time | ||||
|         CoroutineScope(IO).launch { FileHelper.saveTempTrackSuspended(this@TrackerService, track) } | ||||
|         // save state | ||||
|         val context: Context = this as Context | ||||
|         CoroutineScope(IO).launch { track.save_temp_suspended(context) } | ||||
| 
 | ||||
|         trackingState = Keys.STATE_TRACKING_STOPPED | ||||
|         trackingState = Keys.STATE_TRACKING_PAUSED | ||||
|         PreferencesHelper.saveTrackingState(trackingState) | ||||
|         // reset altitude values queue | ||||
| 
 | ||||
|         altitudeValues.reset() | ||||
|         // stop recording steps and location fixes | ||||
| 
 | ||||
|         sensorManager.unregisterListener(this) | ||||
|         handler.removeCallbacks(periodicTrackUpdate) | ||||
|         // update notification | ||||
| 
 | ||||
|         displayNotification() | ||||
|         stopForeground(false) | ||||
|     } | ||||
| 
 | ||||
|     /* Clear track recording */ | ||||
|     fun clearTrack() { | ||||
|     fun clearTrack() | ||||
|     { | ||||
|         track = Track() | ||||
|         FileHelper.deleteTempFile(this) | ||||
|         resumed = false | ||||
|         FileHelper.delete_temp_file(this as Context) | ||||
|         trackingState = Keys.STATE_TRACKING_NOT_STARTED | ||||
|         PreferencesHelper.saveTrackingState(trackingState) | ||||
|         stopForeground(true) | ||||
|         notificationManager.cancel(Keys.TRACKER_SERVICE_NOTIFICATION_ID) // this call was not necessary prior to Android 12 | ||||
|     } | ||||
| 
 | ||||
|     /* Creates location listener */ | ||||
|     private fun createLocationListener(): LocationListener { | ||||
|     private fun createLocationListener(): LocationListener | ||||
|     { | ||||
|         return object : LocationListener { | ||||
|             override fun onLocationChanged(location: Location) { | ||||
|                 // update currentBestLocation if a better location is available | ||||
|  | @ -268,7 +267,8 @@ class TrackerService: Service(), SensorEventListener { | |||
|                     currentBestLocation = location | ||||
|                 } | ||||
|             } | ||||
|             override fun onProviderEnabled(provider: String) { | ||||
|             override fun onProviderEnabled(provider: String) | ||||
|             { | ||||
|                 LogHelper.v(TAG, "onProviderEnabled $provider") | ||||
|                 when (provider) { | ||||
|                     LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled( | ||||
|  | @ -280,7 +280,8 @@ class TrackerService: Service(), SensorEventListener { | |||
|                         ) | ||||
|                 } | ||||
|             } | ||||
|             override fun onProviderDisabled(provider: String) { | ||||
|             override fun onProviderDisabled(provider: String) | ||||
|             { | ||||
|                 LogHelper.v(TAG, "onProviderDisabled $provider") | ||||
|                 when (provider) { | ||||
|                     LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled( | ||||
|  | @ -292,59 +293,58 @@ class TrackerService: Service(), SensorEventListener { | |||
|                         ) | ||||
|                 } | ||||
|             } | ||||
|             override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) { | ||||
|             override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) | ||||
|             { | ||||
|                 // deprecated method | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Adds a GPS location listener to location manager */ | ||||
|     private fun addGpsLocationListener() { | ||||
|         // check if already registered | ||||
|         if (!gpsLocationListenerRegistered) { | ||||
|             // check if Network provider is available | ||||
|             gpsProviderActive = LocationHelper.isGpsEnabled(locationManager) | ||||
|             if (gpsProviderActive) { | ||||
|                 // check for location permission | ||||
|                 if (ContextCompat.checkSelfPermission( | ||||
|                         this, | ||||
|                         Manifest.permission.ACCESS_FINE_LOCATION | ||||
|                     ) == PackageManager.PERMISSION_GRANTED) { | ||||
|                     // adds GPS location listener | ||||
|                     locationManager.requestLocationUpdates( | ||||
|                         LocationManager.GPS_PROVIDER, | ||||
|                         0, | ||||
|                         0f, | ||||
|                         gpsLocationListener | ||||
|                     ) | ||||
|                     gpsLocationListenerRegistered = true | ||||
|                     LogHelper.v(TAG, "Added GPS location listener.") | ||||
|                 } else { | ||||
|                     LogHelper.w( | ||||
|                         TAG, | ||||
|                         "Unable to add GPS location listener. Location permission is not granted." | ||||
|                     ) | ||||
|                 } | ||||
|             } else { | ||||
|                 LogHelper.w(TAG, "Unable to add GPS location listener.") | ||||
|             } | ||||
|         } else { | ||||
|             LogHelper.v(TAG, "Skipping registration. GPS location listener has already been added.") | ||||
|     private fun addGpsLocationListener() | ||||
|     { | ||||
|         if (gpsLocationListenerRegistered) | ||||
|         { | ||||
|             LogHelper.v(TAG, "GPS location listener has already been added.") | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         gpsProviderActive = LocationHelper.isGpsEnabled(locationManager) | ||||
|         if (! gpsProviderActive) | ||||
|         { | ||||
|             LogHelper.w(TAG, "Device GPS is not enabled.") | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         val has_permission: Boolean = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED | ||||
|         if (! has_permission) | ||||
|         { | ||||
|             LogHelper.w(TAG, "Location permission is not granted.") | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         locationManager.requestLocationUpdates( | ||||
|             LocationManager.GPS_PROVIDER, | ||||
|             0, | ||||
|             0f, | ||||
|             gpsLocationListener | ||||
|         ) | ||||
|         gpsLocationListenerRegistered = true | ||||
|         LogHelper.v(TAG, "Added GPS location listener.") | ||||
|     } | ||||
| 
 | ||||
|     /* Adds a Network location listener to location manager */ | ||||
|     private fun addNetworkLocationListener() { | ||||
|         if (gpsOnly) | ||||
|         { | ||||
|             LogHelper.v(TAG, "User prefers GPS-only.") | ||||
|             return; | ||||
|             LogHelper.v(TAG, "Skipping Network listener. User prefers GPS-only.") | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (networkLocationListenerRegistered) | ||||
|         { | ||||
|             LogHelper.v(TAG, "Network location listener has already been added.") | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         networkProviderActive = LocationHelper.isNetworkEnabled(locationManager) | ||||
|  | @ -378,10 +378,7 @@ class TrackerService: Service(), SensorEventListener { | |||
|             gpsLocationListenerRegistered = false | ||||
|             LogHelper.v(TAG, "Removed GPS location listener.") | ||||
|         } else { | ||||
|             LogHelper.w( | ||||
|                 TAG, | ||||
|                 "Unable to remove GPS location listener. Location permission is needed." | ||||
|             ) | ||||
|             LogHelper.w(TAG, "Unable to remove GPS location listener. Location permission is needed.") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -392,10 +389,7 @@ class TrackerService: Service(), SensorEventListener { | |||
|             networkLocationListenerRegistered = false | ||||
|             LogHelper.v(TAG, "Removed Network location listener.") | ||||
|         } else { | ||||
|             LogHelper.w( | ||||
|                 TAG, | ||||
|                 "Unable to remove Network location listener. Location permission is needed." | ||||
|             ) | ||||
|             LogHelper.w(TAG, "Unable to remove Network location listener. Location permission is needed.") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -424,25 +418,25 @@ class TrackerService: Service(), SensorEventListener { | |||
|      * Defines the listener for changes in shared preferences | ||||
|      */ | ||||
|     private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> | ||||
|             when (key) { | ||||
|                 // preference "Restrict to GPS" | ||||
|                 Keys.PREF_GPS_ONLY -> { | ||||
|                     gpsOnly = PreferencesHelper.loadGpsOnly() | ||||
|                     when (gpsOnly) { | ||||
|                         true -> removeNetworkLocationListener() | ||||
|                         false -> addNetworkLocationListener() | ||||
|                     } | ||||
|                 } | ||||
|                 // preference "Use Imperial Measurements" | ||||
|                 Keys.PREF_USE_IMPERIAL_UNITS -> { | ||||
|                     useImperial = PreferencesHelper.loadUseImperialUnits() | ||||
|                 } | ||||
|                 // preference "Recording Accuracy" | ||||
|                 Keys.PREF_OMIT_RESTS -> { | ||||
|                     omitRests = PreferencesHelper.loadOmitRests() | ||||
|         when (key) { | ||||
|             // preference "Restrict to GPS" | ||||
|             Keys.PREF_GPS_ONLY -> { | ||||
|                 gpsOnly = PreferencesHelper.loadGpsOnly() | ||||
|                 when (gpsOnly) { | ||||
|                     true -> removeNetworkLocationListener() | ||||
|                     false -> addNetworkLocationListener() | ||||
|                 } | ||||
|             } | ||||
|             // preference "Use Imperial Measurements" | ||||
|             Keys.PREF_USE_IMPERIAL_UNITS -> { | ||||
|                 useImperial = PreferencesHelper.loadUseImperialUnits() | ||||
|             } | ||||
|             // preference "Recording Accuracy" | ||||
|             Keys.PREF_OMIT_RESTS -> { | ||||
|                 omitRests = PreferencesHelper.loadOmitRests() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     /* | ||||
|      * End of declaration | ||||
|      */ | ||||
|  | @ -460,19 +454,13 @@ class TrackerService: Service(), SensorEventListener { | |||
|     /* | ||||
|      * Runnable: Periodically track updates (if recording active) | ||||
|      */ | ||||
|     private val periodicTrackUpdate: Runnable = object : Runnable { | ||||
|     private val periodicTrackUpdate: Runnable = object : Runnable | ||||
|     { | ||||
|         override fun run() { | ||||
|             // add waypoint to track - step count is continuously updated in onSensorChanged | ||||
|             val result: Pair<Boolean, Track> = TrackHelper.addWayPointToTrack(track, currentBestLocation, omitRests, resumed) | ||||
|             // get results | ||||
|             val successfullyAdded: Boolean = result.first | ||||
|             track = result.second | ||||
|             // check, if waypoint was added | ||||
|             if (successfullyAdded) { | ||||
|                 // reset resumed flag, if necessary | ||||
|                 if (resumed) { | ||||
|                     resumed = false | ||||
|                 } | ||||
|             val success = track.add_waypoint(currentBestLocation, omitRests, resumed) | ||||
|             if (success) { | ||||
|                 resumed = false | ||||
| 
 | ||||
|                 // store previous smoothed altitude | ||||
|                 val previousAltitude: Double = altitudeValues.getAverage() | ||||
|  | @ -480,7 +468,7 @@ class TrackerService: Service(), SensorEventListener { | |||
|                 val currentBestLocationAltitude: Double = currentBestLocation.altitude | ||||
|                 if (currentBestLocationAltitude != Keys.DEFAULT_ALTITUDE) altitudeValues.add(currentBestLocationAltitude) | ||||
|                 // TODO remove | ||||
|                 // uncomment to use test altitude values - useful if testing wirth an emulator | ||||
|                 // uncomment to use test altitude values - useful if testing with an emulator | ||||
|                 //altitudeValues.add(getTestAltitude()) // TODO remove | ||||
|                 // TODO remove | ||||
| 
 | ||||
|  | @ -499,7 +487,7 @@ class TrackerService: Service(), SensorEventListener { | |||
|                 val now: Date = GregorianCalendar.getInstance().time | ||||
|                 if (now.time - lastSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) { | ||||
|                     lastSave = now | ||||
|                     CoroutineScope(IO).launch { FileHelper.saveTempTrackSuspended(this@TrackerService, track) } | ||||
|                     CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) } | ||||
|                 } | ||||
|             } | ||||
|             // update notification | ||||
|  | @ -514,10 +502,12 @@ class TrackerService: Service(), SensorEventListener { | |||
| 
 | ||||
|     /* Simple queue that evicts older elements and holds an average */ | ||||
|     /* Credit: CircularQueue https://stackoverflow.com/a/51923797 */ | ||||
|     class SimpleMovingAverageQueue(var capacity: Int) : LinkedList<Double>() { | ||||
|     class SimpleMovingAverageQueue(var capacity: Int) : LinkedList<Double>() | ||||
|     { | ||||
|         var prepared: Boolean = false | ||||
|         private var sum: Double = 0.0 | ||||
|         override fun add(element: Double): Boolean { | ||||
|         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 | ||||
|  | @ -526,8 +516,12 @@ class TrackerService: Service(), SensorEventListener { | |||
|             sum += element | ||||
|             return super.add(element) | ||||
|         } | ||||
|         fun getAverage(): Double = sum / this.size | ||||
|         fun reset() { | ||||
|         fun getAverage(): Double | ||||
|         { | ||||
|             return sum / this.size | ||||
|         } | ||||
|         fun reset() | ||||
|         { | ||||
|             this.clear() | ||||
|             prepared = false | ||||
|             sum = 0.0 | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ import android.view.LayoutInflater | |||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.constraintlayout.widget.ConstraintLayout | ||||
| import androidx.core.net.toUri | ||||
| import androidx.core.os.bundleOf | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.navigation.fragment.findNavController | ||||
|  | @ -33,11 +34,8 @@ import androidx.recyclerview.widget.LinearLayoutManager | |||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import kotlinx.coroutines.* | ||||
| import kotlinx.coroutines.Dispatchers.Main | ||||
| import org.y20k.trackbook.core.Tracklist | ||||
| import org.y20k.trackbook.core.TracklistElement | ||||
| import org.y20k.trackbook.helpers.FileHelper | ||||
| import org.y20k.trackbook.core.Track | ||||
| import org.y20k.trackbook.helpers.LogHelper | ||||
| import org.y20k.trackbook.helpers.TrackHelper | ||||
| import org.y20k.trackbook.helpers.UiHelper | ||||
| import org.y20k.trackbook.tracklist.TracklistAdapter | ||||
| 
 | ||||
|  | @ -95,19 +93,17 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, | |||
|         return rootView | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Overrides onTrackElementTapped from TracklistElementAdapterListener */ | ||||
|     override fun onTrackElementTapped(tracklistElement: TracklistElement) { | ||||
|     override fun onTrackElementTapped(track: Track) { | ||||
|         val bundle: Bundle = bundleOf( | ||||
|             Keys.ARG_TRACK_TITLE to tracklistElement.name, | ||||
|             Keys.ARG_TRACK_FILE_URI to tracklistElement.trackUriString, | ||||
|             Keys.ARG_GPX_FILE_URI to tracklistElement.gpxUriString, | ||||
|             Keys.ARG_TRACK_ID to tracklistElement.id | ||||
|             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 | ||||
|         ) | ||||
|         findNavController().navigate(R.id.fragment_track, bundle) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Overrides onYesNoDialog from YesNoDialogListener */ | ||||
|     override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) { | ||||
|         CoroutineScope(Dispatchers.IO).launch { | ||||
|  | @ -117,7 +113,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, | |||
|                         // user tapped remove track | ||||
|                         true -> { | ||||
|                             toggleOnboardingLayout() | ||||
|                             val deferred: Deferred<Unit> = async { tracklistAdapter.removeTrackAtPositionSuspended(activity as Context, payload) } | ||||
|                             val deferred: Deferred<Unit> = async { tracklistAdapter.delete_track_at_position_suspended(activity as Context, payload) } | ||||
|                             // wait for result and store in tracklist | ||||
|                             withContext(Main) { | ||||
|                                 deferred.await() | ||||
|  | @ -173,7 +169,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, | |||
|                 return; | ||||
|             } | ||||
|             CoroutineScope(Main). launch { | ||||
|                 tracklistAdapter.removeTrackById(this@TracklistFragment.activity as Context, deleteTrackId) | ||||
|                 tracklistAdapter.delete_track_by_id(this@TracklistFragment.activity as Context, deleteTrackId) | ||||
|                 toggleOnboardingLayout() | ||||
|             } | ||||
|         } | ||||
|  |  | |||
|  | @ -17,56 +17,280 @@ | |||
| package org.y20k.trackbook.core | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.location.Location | ||||
| import android.os.Parcelable | ||||
| import android.util.Log | ||||
| import androidx.annotation.Keep | ||||
| import com.google.gson.annotations.Expose | ||||
| import java.io.File | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.* | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.suspendCoroutine | ||||
| import kotlin.random.Random | ||||
| import kotlinx.parcelize.Parcelize | ||||
| import org.y20k.trackbook.Keys | ||||
| import org.y20k.trackbook.helpers.DateTimeHelper | ||||
| import org.y20k.trackbook.helpers.FileHelper | ||||
| import org.y20k.trackbook.helpers.LocationHelper | ||||
| 
 | ||||
| /* | ||||
|  * Track data class | ||||
|  */ | ||||
| @Keep | ||||
| @Parcelize | ||||
| data class Track (@Expose val id: Long = make_random_id(), | ||||
|                   @Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION, | ||||
|                   @Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(), | ||||
|                   @Expose var distance: Float = 0f, | ||||
|                   @Expose var duration: Long = 0L, | ||||
|                   @Expose var recordingPaused: Long = 0L, | ||||
|                   @Expose var stepCount: Float = 0f, | ||||
|                   @Expose var recordingStart: Date = GregorianCalendar.getInstance().time, | ||||
|                   @Expose var recordingStop: Date = recordingStart, | ||||
|                   @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 trackUriString: String = String(), | ||||
|                   @Expose var gpxUriString: String = String(), | ||||
|                   @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 = String()): Parcelable | ||||
| data class Track ( | ||||
|     @Expose val id: Long = make_random_id(), | ||||
|     @Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION, | ||||
|     @Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(), | ||||
|     @Expose var distance: Float = 0f, | ||||
|     @Expose var duration: Long = 0L, | ||||
|     @Expose var recordingPaused: Long = 0L, | ||||
|     @Expose var stepCount: Float = 0f, | ||||
|     @Expose var recordingStart: Date = GregorianCalendar.getInstance().time, | ||||
|     @Expose var dateString: String = DateTimeHelper.convertToReadableDate(recordingStart), | ||||
|     @Expose var recordingStop: Date = recordingStart, | ||||
|     @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 | ||||
| { | ||||
|     /* Creates a TracklistElement */ | ||||
|     fun toTracklistElement(context: Context): TracklistElement { | ||||
|         val readableDateString: String = DateTimeHelper.convertToReadableDate(recordingStart) | ||||
|         val readableDurationString: String = DateTimeHelper.convertToReadableTime(context, duration) | ||||
|         return TracklistElement( | ||||
|             id = id, | ||||
|             name = name, | ||||
|             date = recordingStart, | ||||
|             dateString = readableDateString, | ||||
|             distance = distance, | ||||
|             duration = duration, | ||||
|             trackUriString = trackUriString, | ||||
|             gpxUriString = gpxUriString, | ||||
|             starred = false | ||||
|     fun add_waypoint(location: Location, omitRests: Boolean, resumed: Boolean): Boolean | ||||
|     { | ||||
|         // Step 1: Get previous location | ||||
|         val previousLocation: Location? | ||||
|         var numberOfWayPoints: Int = this.wayPoints.size | ||||
| 
 | ||||
|         // CASE: First location | ||||
|         if (numberOfWayPoints == 0) | ||||
|         { | ||||
|             previousLocation = null | ||||
|         } | ||||
|         // CASE: Second location - check if first location was plausible & remove implausible location | ||||
|         else if (numberOfWayPoints == 1 && !LocationHelper.isFirstLocationPlausible(location, this)) | ||||
|         { | ||||
|             previousLocation = null | ||||
|             numberOfWayPoints = 0 | ||||
|             this.wayPoints.removeAt(0) | ||||
|         } | ||||
|         // CASE: Third location or second location (if first was plausible) | ||||
|         else | ||||
|         { | ||||
|             previousLocation = this.wayPoints[numberOfWayPoints - 1].toLocation() | ||||
|         } | ||||
| 
 | ||||
|         // Step 2: Update duration | ||||
|         val now: Date = GregorianCalendar.getInstance().time | ||||
|         val difference: Long = now.time - this.recordingStop.time | ||||
|         this.duration += difference | ||||
|         this.recordingStop = now | ||||
| 
 | ||||
|         // Step 3: Add waypoint, if recent and accurate and different enough | ||||
|         val shouldBeAdded: Boolean = ( | ||||
|             LocationHelper.isRecentEnough(location) && | ||||
|             LocationHelper.isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY) && | ||||
|             LocationHelper.isDifferentEnough(previousLocation, location, omitRests) | ||||
|         ) | ||||
|         if (! shouldBeAdded) | ||||
|         { | ||||
|             return false | ||||
|         } | ||||
|         // 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)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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_json_file(context: Context): File | ||||
|     { | ||||
|         val basename: String = this.id.toString() + Keys.TRACKBOOK_FILE_EXTENSION | ||||
|         return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), basename) | ||||
|     } | ||||
| 
 | ||||
|     fun save_both(context: Context) | ||||
|     { | ||||
|         this.save_json(context) | ||||
|         this.save_gpx(context) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun save_both_suspended(context: Context) | ||||
|     { | ||||
|         return suspendCoroutine { cont -> | ||||
|             cont.resume(this.save_both(context)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun save_gpx(context: Context) | ||||
|     { | ||||
|         val gpx: String = this.to_gpx() | ||||
|         FileHelper.write_text_file_noblank(gpx, this.get_gpx_file(context)) | ||||
|         Log.i("VOUSSOIR", "Saved ${this.id}.gpx") | ||||
|     } | ||||
| 
 | ||||
|     suspend fun save_gpx_suspended(context: Context) | ||||
|     { | ||||
|         return suspendCoroutine { cont -> | ||||
|             cont.resume(this.save_gpx(context)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun save_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(""" | ||||
|         <?xml version="1.0" encoding="UTF-8" standalone="no" ?> | ||||
|         <gpx | ||||
|             version="1.1" creator="Trackbook App (Android)" | ||||
|             xmlns="http://www.topografix.com/GPX/1/1" | ||||
|             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|             xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" | ||||
|         > | ||||
|         """.trimIndent()) | ||||
|         gpxString.appendLine("\t<metadata>") | ||||
|         gpxString.appendLine("\t\t<name>Trackbook Recording: ${this.name}</name>") | ||||
|         gpxString.appendLine("\t</metadata>") | ||||
| 
 | ||||
|         // POIs | ||||
|         val poiList: List<WayPoint> =  this.wayPoints.filter { it.starred } | ||||
|         poiList.forEach { poi -> | ||||
|             gpxString.appendLine("\t<wpt lat=\"${poi.latitude}\" lon=\"${poi.longitude}\">") | ||||
|             gpxString.appendLine("\t\t<name>Point of interest</name>") | ||||
|             gpxString.appendLine("\t\t<ele>${poi.altitude}</ele>") | ||||
|             gpxString.appendLine("\t</wpt>") | ||||
|         } | ||||
| 
 | ||||
|         // TRK | ||||
|         val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) | ||||
|         dateFormat.timeZone = TimeZone.getTimeZone("UTC") | ||||
|         gpxString.appendLine("\t<trk>") | ||||
|         gpxString.appendLine("\t\t<name>${this.name}</name>") | ||||
|         gpxString.appendLine("\t\t<trkseg>") | ||||
|         this.wayPoints.forEach { wayPoint -> | ||||
|             gpxString.appendLine("\t\t\t<trkpt lat=\"${wayPoint.latitude}\" lon=\"${wayPoint.longitude}\">") | ||||
|             gpxString.appendLine("\t\t\t\t<ele>${wayPoint.altitude}</ele>") | ||||
|             gpxString.appendLine("\t\t\t\t<time>${dateFormat.format(Date(wayPoint.time))}</time>") | ||||
|             gpxString.appendLine("\t\t\t\t<sat>${wayPoint.numberSatellites}</sat>") | ||||
|             gpxString.appendLine("\t\t\t</trkpt>") | ||||
|         } | ||||
|         gpxString.appendLine("\t\t</trkseg>") | ||||
|         gpxString.appendLine("\t</trk>") | ||||
|         gpxString.appendLine("</gpx>") | ||||
| 
 | ||||
|         return gpxString.toString() | ||||
|     } | ||||
| 
 | ||||
|     fun to_json(): String | ||||
|     { | ||||
|         return FileHelper.getCustomGson().toJson(this) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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()) | ||||
|     { | ||||
|         return Track() | ||||
|     } | ||||
|     return FileHelper.getCustomGson().fromJson(json, Track::class.java) | ||||
| } | ||||
| 
 | ||||
| fun make_random_id(): Long | ||||
|  |  | |||
|  | @ -17,54 +17,79 @@ | |||
| 
 | ||||
| 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 | ||||
| import org.y20k.trackbook.helpers.TrackHelper | ||||
| import java.util.* | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
|  * Tracklist data class | ||||
|  */ | ||||
| @Keep | ||||
| @Parcelize | ||||
| data class Tracklist (@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION, | ||||
|                       @Expose val tracklistElements: MutableList<TracklistElement> = mutableListOf<TracklistElement>()): Parcelable { | ||||
| 
 | ||||
|     /* Return trackelement for given track id */ | ||||
|     fun getTrackElement(trackId: Long): TracklistElement? { | ||||
|         tracklistElements.forEach { tracklistElement -> | ||||
|             if (tracklistElement.id == trackId) { | ||||
|                 return tracklistElement | ||||
| data class Tracklist ( | ||||
|     @Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION, | ||||
|     @Expose val tracks: MutableList<Track> = mutableListOf<Track>() | ||||
| ): Parcelable | ||||
| { | ||||
|     fun delete_non_starred(context: Context) | ||||
|     { | ||||
|         val to_delete: List<Track> = this.tracks.filter{! it.starred} | ||||
|         to_delete.forEach { track -> | ||||
|             if (!track.starred) | ||||
|             { | ||||
|                 track.delete(context) | ||||
|             } | ||||
|         } | ||||
|         return null | ||||
|         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(): Float | ||||
|     fun get_total_distance(): Double | ||||
|     { | ||||
|         var total: Float = 0F | ||||
|         tracklistElements.forEach { tracklist_element -> | ||||
|             total += tracklist_element.distance | ||||
|         } | ||||
|         return total | ||||
|         return this.tracks.sumOf {it.distance.toDouble()} | ||||
|     } | ||||
| 
 | ||||
|     fun get_total_duration(): Long | ||||
|     { | ||||
|         var total: Long = 0L | ||||
|         tracklistElements.forEach { tracklist_element -> | ||||
|             total += tracklist_element.duration | ||||
|         } | ||||
|         return total | ||||
|         return this.tracks.sumOf {it.duration} | ||||
|     } | ||||
| 
 | ||||
|     /* Create a deep copy */ | ||||
|     fun deepCopy(): Tracklist { | ||||
|         return Tracklist(tracklistFormatVersion, mutableListOf<TracklistElement>().apply { addAll(tracklistElements) }) | ||||
|     fun deepCopy(): Tracklist | ||||
|     { | ||||
|         return Tracklist(tracklistFormatVersion, mutableListOf<Track>().apply { addAll(tracks) }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| fun load_tracklist(context: Context): Tracklist { | ||||
|     Log.i("VOUSSOIR", "Loading tracklist.") | ||||
|     val folder = context.getExternalFilesDir("tracks") | ||||
|     var 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))} | ||||
| } | ||||
|  |  | |||
|  | @ -1,43 +0,0 @@ | |||
| /* | ||||
|  * TracklistElement.kt | ||||
|  * Implements the TracklistElement data class | ||||
|  * A TracklistElement data about a Track | ||||
|  * | ||||
|  * 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.os.Parcelable | ||||
| import androidx.annotation.Keep | ||||
| import com.google.gson.annotations.Expose | ||||
| import kotlinx.parcelize.Parcelize | ||||
| import java.util.* | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
|  * TracklistElement data class | ||||
|  */ | ||||
| @Keep | ||||
| @Parcelize | ||||
| data class TracklistElement( | ||||
|     @Expose val id: Long, | ||||
|     @Expose var name: String, | ||||
|     @Expose val date: Date, | ||||
|     @Expose val dateString: String, | ||||
|     @Expose val duration: Long, | ||||
|     @Expose val distance: Float, | ||||
|     @Expose val trackUriString: String, | ||||
|     @Expose val gpxUriString: String, | ||||
|     @Expose var starred: Boolean = false | ||||
| ) : Parcelable { | ||||
| } | ||||
|  | @ -18,26 +18,16 @@ | |||
| package org.y20k.trackbook.helpers | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.database.Cursor | ||||
| import android.graphics.Bitmap | ||||
| import android.net.Uri | ||||
| import android.provider.OpenableColumns | ||||
| import android.util.Log | ||||
| import androidx.core.net.toFile | ||||
| import androidx.core.net.toUri | ||||
| import com.google.gson.Gson | ||||
| import com.google.gson.GsonBuilder | ||||
| import org.y20k.trackbook.Keys | ||||
| import org.y20k.trackbook.core.Track | ||||
| import org.y20k.trackbook.core.Tracklist | ||||
| import org.y20k.trackbook.core.TracklistElement | ||||
| import java.io.* | ||||
| import java.text.NumberFormat | ||||
| import java.util.* | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.suspendCoroutine | ||||
| import kotlin.math.ln | ||||
| import kotlin.math.pow | ||||
| import org.y20k.trackbook.Keys | ||||
| import org.y20k.trackbook.core.Track | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
|  | @ -48,185 +38,29 @@ object FileHelper { | |||
|     /* Define log tag */ | ||||
|     private val TAG: String = LogHelper.makeLogTag(FileHelper::class.java) | ||||
| 
 | ||||
| 
 | ||||
|     /* Return an InputStream for given Uri */ | ||||
|     fun getTextFileStream(context: Context, uri: Uri): InputStream? { | ||||
|         var stream : InputStream? = null | ||||
|         try { | ||||
|             stream = context.contentResolver.openInputStream(uri) | ||||
|         } catch (e : Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|         return stream | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Get file size for given Uri */ | ||||
|     fun getFileSize(context: Context, uri: Uri): Long { | ||||
|         val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) | ||||
|         if (cursor != null) { | ||||
|             val sizeIndex: Int = cursor.getColumnIndex(OpenableColumns.SIZE) | ||||
|             cursor.moveToFirst() | ||||
|             val size: Long = cursor.getLong(sizeIndex) | ||||
|             cursor.close() | ||||
|             return size | ||||
|         } else { | ||||
|             return 0L | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Get file name for given Uri */ | ||||
|     fun getFileName(context: Context, uri: Uri): String { | ||||
|         val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) | ||||
|         if (cursor != null) { | ||||
|             val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) | ||||
|             cursor.moveToFirst() | ||||
|             val name: String = cursor.getString(nameIndex) | ||||
|             cursor.close() | ||||
|             return name | ||||
|         } else { | ||||
|             return String() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Clears given folder - keeps given number of files */ | ||||
|     fun clearFolder(folder: File?, keep: Int, deleteFolder: Boolean = false) { | ||||
|         if (folder != null && folder.exists()) { | ||||
|             val files = folder.listFiles() | ||||
|             val fileCount: Int = files.size | ||||
|             files.sortBy { it.lastModified() } | ||||
|             for (fileNumber in files.indices) { | ||||
|                 if (fileNumber < fileCount - keep) { | ||||
|                     files[fileNumber].delete() | ||||
|                 } | ||||
|             } | ||||
|             if (deleteFolder && keep == 0) { | ||||
|                 folder.delete() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Reads tracklist from storage using GSON */ | ||||
|     fun readTracklist(context: Context): Tracklist { | ||||
|         LogHelper.v(TAG, "Reading Tracklist - Thread: ${Thread.currentThread().name}") | ||||
|         var folder = context.getExternalFilesDir("tracks") | ||||
|         var tracklist: Tracklist = Tracklist() | ||||
|         Log.i(TAG, folder.toString()) | ||||
|         if (folder != null) | ||||
|     fun delete_temp_file(context: Context) | ||||
|     { | ||||
|         val temp: File = get_temp_file(context) | ||||
|         if (temp.isFile()) | ||||
|         { | ||||
|             folder.walk().filter{ f: File -> f.isFile }.forEach{ track_file -> | ||||
|                 val track_json = readTextFile(context, track_file.toUri()) | ||||
|                 Log.i("VOUSSOIR", track_json) | ||||
|                 val track = getCustomGson().fromJson(track_json, Track::class.java) | ||||
|                 val tracklist_element = track.toTracklistElement(context) | ||||
|                 tracklist.tracklistElements.add(tracklist_element) | ||||
|             } | ||||
|             temp.delete() | ||||
|         } | ||||
|         return tracklist | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Reads track from storage using GSON */ | ||||
|     fun readTrack(context: Context, fileUri: Uri): Track { | ||||
|         // get JSON from text file | ||||
|         val json: String = readTextFile(context, fileUri) | ||||
|         var track: Track = Track() | ||||
|         when (json.isNotEmpty()) { | ||||
|             // convert JSON and return as track | ||||
|             true -> try { | ||||
|                 track = getCustomGson().fromJson(json, Track::class.java) | ||||
|             } catch (e: Exception) { | ||||
|                 e.printStackTrace() | ||||
|             } | ||||
|         } | ||||
|         return track | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Deletes temp track file */ | ||||
|     fun deleteTempFile(context: Context) { | ||||
|         getTempFileUri(context).toFile().delete() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Checks if temp track file exists */ | ||||
|     fun tempFileExists(context: Context): Boolean { | ||||
|         return getTempFileUri(context).toFile().exists() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Creates Uri for Gpx file of a track */ | ||||
|     fun getGpxFileUri(context: Context, track: Track): Uri = File(context.getExternalFilesDir(Keys.FOLDER_GPX), getGpxFileName(track)).toUri() | ||||
| 
 | ||||
| 
 | ||||
|     /* Creates file name for Gpx file of a track */ | ||||
|     fun getGpxFileName(track: Track): String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.GPX_FILE_EXTENSION | ||||
| 
 | ||||
| 
 | ||||
|     /* Creates Uri for json track file */ | ||||
|     fun getTrackFileUri(context: Context, track: Track): Uri { | ||||
|         val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.TRACKBOOK_FILE_EXTENSION | ||||
|         return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), fileName).toUri() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Creates Uri for json temp track file */ | ||||
|     fun getTempFileUri(context: Context): Uri { | ||||
|         return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE).toUri() | ||||
|     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 -> | ||||
|             cont.resume(renameTrack(context, track, newName)) | ||||
|             track.name = newName | ||||
|             track.save_both(context) | ||||
|             cont.resume(Unit) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Suspend function: Wrapper for saveTrack */ | ||||
|     suspend fun saveTrackSuspended(track: Track, saveGpxToo: Boolean) { | ||||
|         return suspendCoroutine { cont -> | ||||
|             cont.resume(saveTrack(track, saveGpxToo)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Suspend function: Wrapper for saveTempTrack */ | ||||
|     suspend fun saveTempTrackSuspended(context: Context, track: Track) { | ||||
|         return suspendCoroutine { cont -> | ||||
|             cont.resume(saveTempTrack(context, track)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Suspend function: Wrapper for deleteTrack */ | ||||
|     suspend fun deleteTrackSuspended(context: Context, tracklist_element: TracklistElement, tracklist: Tracklist): Tracklist { | ||||
|         return suspendCoroutine { cont -> | ||||
|             cont.resume(deleteTrack(context, tracklist_element, tracklist)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Suspend function: Deletes tracks that are not starred using deleteTracks */ | ||||
|     suspend fun deleteNonStarredSuspended(context: Context, tracklist: Tracklist): Tracklist { | ||||
|         return suspendCoroutine { cont -> | ||||
|             val tracklistElements = mutableListOf<TracklistElement>() | ||||
|             tracklist.tracklistElements.forEach { tracklistElement -> | ||||
|                 if (!tracklistElement.starred) { | ||||
|                     tracklistElements.add(tracklistElement) | ||||
|                 } | ||||
|             } | ||||
|             cont.resume(deleteTracks(context, tracklistElements, tracklist)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Suspend function: Wrapper for readTracklist */ | ||||
|     suspend fun readTracklistSuspended(context: Context): Tracklist { | ||||
|         return suspendCoroutine {cont -> | ||||
|             cont.resume(readTracklist(context)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Suspend function: Wrapper for copyFile */ | ||||
|     suspend fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) { | ||||
|  | @ -235,83 +69,6 @@ object FileHelper { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Save Track as JSON to storage */ | ||||
|     private fun saveTrack(track: Track, saveGpxToo: Boolean) { | ||||
|         val jsonString: String = getTrackJsonString(track) | ||||
|         if (jsonString.isNotBlank()) { | ||||
|             // write track file | ||||
|             writeTextFile(jsonString, track.trackUriString.toUri()) | ||||
|         } | ||||
|         if (saveGpxToo) { | ||||
|             val gpxString: String = TrackHelper.createGpxString(track) | ||||
|             if (gpxString.isNotBlank()) { | ||||
|                 // write GPX file | ||||
|                 writeTextFile(gpxString, track.gpxUriString.toUri()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Save Temp Track as JSON to storage */ | ||||
|     private fun saveTempTrack(context: Context, track: Track) { | ||||
|         val json: String = getTrackJsonString(track) | ||||
|         if (json.isNotBlank()) { | ||||
|             writeTextFile(json, getTempFileUri(context)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Creates Uri for tracklist file */ | ||||
|     private fun getTracklistFileUri(context: Context): Uri { | ||||
|         return File(context.getExternalFilesDir(""), Keys.TRACKLIST_FILE).toUri() | ||||
|     } | ||||
| 
 | ||||
|     /* Renames track */ | ||||
|     private fun renameTrack(context: Context, track: Track, newName: String) { | ||||
|         // search track in tracklist | ||||
|         val tracklist: Tracklist = readTracklist(context) | ||||
|         var trackUriString: String = String() | ||||
|         tracklist.tracklistElements.forEach { tracklistElement -> | ||||
|             if (tracklistElement.id == track.id) { | ||||
|                 // rename tracklist element | ||||
|                 tracklistElement.name = newName | ||||
|                 trackUriString = tracklistElement.trackUriString | ||||
|             } | ||||
|         } | ||||
|         if (trackUriString.isNotEmpty()) { | ||||
|             // rename track | ||||
|             track.name = newName | ||||
|             // save track | ||||
|             saveTrack(track, saveGpxToo = true) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Deletes multiple tracks */ | ||||
|     private fun deleteTracks(context: Context, tracklistElements: MutableList<TracklistElement>, tracklist: Tracklist): Tracklist { | ||||
|         tracklistElements.forEach { tracklistElement -> | ||||
|             deleteTrack(context, tracklistElement, tracklist) | ||||
|         } | ||||
|         return tracklist | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Deletes one track */ | ||||
|     private fun deleteTrack(context: Context, tracklist_element: TracklistElement, tracklist: Tracklist): Tracklist { | ||||
|         // delete track files | ||||
|         val json_file: File = tracklist_element.trackUriString.toUri().toFile() | ||||
|         if (json_file.isFile) | ||||
|         { | ||||
|             json_file.delete() | ||||
|         } | ||||
|         val gpx_file: File = tracklist_element.gpxUriString.toUri().toFile() | ||||
|         if (gpx_file.isFile) | ||||
|         { | ||||
|             gpx_file.delete() | ||||
|         } | ||||
|         // remove track element from list | ||||
|         tracklist.tracklistElements.removeIf {it.id == tracklist_element.id} | ||||
|         return tracklist | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Copies file to specified target */ | ||||
|     private fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) { | ||||
|  | @ -325,58 +82,19 @@ object FileHelper { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Converts track to JSON */ | ||||
|     private fun getTrackJsonString(track: Track): String { | ||||
|         val gson: Gson = getCustomGson() | ||||
|         var json: String = String() | ||||
|         try { | ||||
|             json = gson.toJson(track) | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|         return json | ||||
|     } | ||||
| 
 | ||||
|     /* Creates a Gson object */ | ||||
|     private fun getCustomGson(): Gson { | ||||
|     fun getCustomGson(): Gson | ||||
|     { | ||||
|         val gsonBuilder = GsonBuilder() | ||||
|         gsonBuilder.setDateFormat("yyyy-MM-dd-HH-mm-ss") | ||||
|         gsonBuilder.excludeFieldsWithoutExposeAnnotation() | ||||
|         return gsonBuilder.create() | ||||
|     } | ||||
| 
 | ||||
|     /* Converts byte value into a human readable format */ | ||||
|     // Source: https://programming.guide/java/formatting-byte-size-to-human-readable-format.html | ||||
|     fun getReadableByteCount(bytes: Long, si: Boolean = true): String { | ||||
| 
 | ||||
|         // check if Decimal prefix symbol (SI) or Binary prefix symbol (IEC) requested | ||||
|         val unit: Long = if (si) 1000L else 1024L | ||||
| 
 | ||||
|         // just return bytes if file size is smaller than requested unit | ||||
|         if (bytes < unit) return "$bytes B" | ||||
| 
 | ||||
|         // calculate exp | ||||
|         val exp: Int = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt() | ||||
| 
 | ||||
|         // determine prefix symbol | ||||
|         val prefix: String = ((if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i") | ||||
| 
 | ||||
|         // calculate result and set number format | ||||
|         val result: Double = bytes / unit.toDouble().pow(exp.toDouble()) | ||||
|         val numberFormat = NumberFormat.getNumberInstance() | ||||
|         numberFormat.maximumFractionDigits = 1 | ||||
| 
 | ||||
|         return numberFormat.format(result) + " " + prefix + "B" | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Reads InputStream from file uri and returns it as String */ | ||||
|     private fun readTextFile(context: Context, fileUri: Uri): String { | ||||
|     fun readTextFile(context: Context, file: File): String { | ||||
|         // todo read https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html | ||||
|         // https://developer.android.com/training/secure-file-sharing/retrieve-info | ||||
|         val file: File = fileUri.toFile() | ||||
|         // check if file exists | ||||
|         if (!file.exists()) { | ||||
|             return String() | ||||
|         } | ||||
|  | @ -393,27 +111,12 @@ object FileHelper { | |||
| 
 | ||||
| 
 | ||||
|     /* Writes given text to file on storage */ | ||||
|     private fun writeTextFile(text: String, fileUri: Uri) { | ||||
|     fun write_text_file_noblank(text: String, file: File) | ||||
|     { | ||||
|         if (text.isNotEmpty()) { | ||||
|             val file: File = fileUri.toFile() | ||||
|             file.writeText(text) | ||||
|         } else { | ||||
|             LogHelper.w(TAG, "Writing text file $fileUri failed. Empty text string text was provided.") | ||||
|             LogHelper.w(TAG, "Writing text file ${file.toUri()} failed. Empty text string was provided.") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Writes given bitmap as image file to storage */ | ||||
|     private fun writeImageFile(context: Context, bitmap: Bitmap, file: File, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, quality: Int = 75) { | ||||
|         if (file.exists()) file.delete () | ||||
|         try { | ||||
|             val out = FileOutputStream(file) | ||||
|             bitmap.compress(format, quality, out) | ||||
|             out.flush() | ||||
|             out.close() | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -17,20 +17,10 @@ | |||
| package org.y20k.trackbook.helpers | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.location.Location | ||||
| import android.widget.Toast | ||||
| import androidx.core.net.toUri | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.* | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers.IO | ||||
| import kotlinx.coroutines.launch | ||||
| import org.y20k.trackbook.Keys | ||||
| import org.y20k.trackbook.R | ||||
| import org.y20k.trackbook.core.Track | ||||
| import org.y20k.trackbook.core.Tracklist | ||||
| import org.y20k.trackbook.core.TracklistElement | ||||
| import org.y20k.trackbook.core.WayPoint | ||||
| 
 | ||||
| /* | ||||
|  * TrackHelper object | ||||
|  | @ -41,123 +31,14 @@ object TrackHelper { | |||
|     private val TAG: String = LogHelper.makeLogTag(TrackHelper::class.java) | ||||
| 
 | ||||
|     /* Adds given locatiom as waypoint to track */ | ||||
|     fun addWayPointToTrack(track: Track, location: Location, omitRests: Boolean, resumed: Boolean): Pair<Boolean, Track> { | ||||
|         // Step 1: Get previous location | ||||
|         val previousLocation: Location? | ||||
|         var numberOfWayPoints: Int = track.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, track)) { | ||||
|             previousLocation = null | ||||
|             numberOfWayPoints = 0 | ||||
|             track.wayPoints.removeAt(0) | ||||
|         } | ||||
|         // CASE: Third location or second location (if first was plausible) | ||||
|         else { | ||||
|             previousLocation = track.wayPoints[numberOfWayPoints - 1].toLocation() | ||||
|         } | ||||
| 
 | ||||
|         // Step 2: Update duration | ||||
|         val now: Date = GregorianCalendar.getInstance().time | ||||
|         val difference: Long = now.time - track.recordingStop.time | ||||
|         track.duration = track.duration + difference | ||||
|         track.recordingStop = now | ||||
| 
 | ||||
|         // Step 3: Add waypoint, ifrecent 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) { | ||||
|             // Step 3.1: Update distance (do not update if resumed -> we do not want to add values calculated during a recording pause) | ||||
|             if (!resumed) { | ||||
|                 track.distance = track.distance + LocationHelper.calculateDistance(previousLocation, location) | ||||
|             } | ||||
|             // Step 3.2: Update altitude values | ||||
|             val altitude: Double = location.altitude | ||||
|             if (altitude != 0.0) { | ||||
|                 if (numberOfWayPoints == 0) { | ||||
|                     track.maxAltitude = altitude | ||||
|                     track.minAltitude = altitude | ||||
|                 } | ||||
|                 else { | ||||
|                     if (altitude > track.maxAltitude) track.maxAltitude = altitude | ||||
|                     if (altitude < track.minAltitude) track.minAltitude = altitude | ||||
|                 } | ||||
|             } | ||||
|             // Step 3.3: Toggle stop over status, if necessary | ||||
|             if (track.wayPoints.size < 0) { | ||||
|                 track.wayPoints[track.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location) | ||||
|             } | ||||
| 
 | ||||
|             // Step 3.4: Add current location as point to center on for later display | ||||
|             track.latitude = location.latitude | ||||
|             track.longitude = location.longitude | ||||
| 
 | ||||
|             // Step 3.5: Add location as new waypoint | ||||
|             track.wayPoints.add(WayPoint(location = location, distanceToStartingPoint = track.distance)) | ||||
|         } | ||||
| 
 | ||||
|         return Pair(shouldBeAdded, track) | ||||
|     } | ||||
| 
 | ||||
|     /* Calculates time passed since last stop of recording */ | ||||
|     fun calculateDurationOfPause(recordingStop: Date): Long = GregorianCalendar.getInstance().time.time - recordingStop.time | ||||
| 
 | ||||
|     /* Creates GPX string for given track */ | ||||
|     fun createGpxString(track: Track): String { | ||||
|         val gpxString = StringBuilder("") | ||||
| 
 | ||||
|         // Header | ||||
|         gpxString.appendLine(""" | ||||
|         <?xml version="1.0" encoding="UTF-8" standalone="no" ?> | ||||
|         <gpx | ||||
|             version="1.1" creator="Trackbook App (Android)" | ||||
|             xmlns="http://www.topografix.com/GPX/1/1" | ||||
|             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|             xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" | ||||
|         > | ||||
|         """.trimIndent()) | ||||
|         gpxString.appendLine("\t<metadata>") | ||||
|         gpxString.appendLine("\t\t<name>Trackbook Recording: ${track.name}</name>") | ||||
|         gpxString.appendLine("\t</metadata>") | ||||
| 
 | ||||
|         // POIs | ||||
|         val poiList: List<WayPoint> =  track.wayPoints.filter { it.starred } | ||||
|         poiList.forEach { poi -> | ||||
|             gpxString.appendLine("\t<wpt lat=\"${poi.latitude}\" lon=\"${poi.longitude}\">") | ||||
|             gpxString.appendLine("\t\t<name>Point of interest</name>") | ||||
|             gpxString.appendLine("\t\t<ele>${poi.altitude}</ele>") | ||||
|             gpxString.appendLine("\t</wpt>") | ||||
|         } | ||||
| 
 | ||||
|         // TRK | ||||
|         val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) | ||||
|         dateFormat.timeZone = TimeZone.getTimeZone("UTC") | ||||
|         gpxString.appendLine("\t<trk>") | ||||
|         gpxString.appendLine("\t\t<name>${track.name}</name>") | ||||
|         gpxString.appendLine("\t\t<trkseg>") | ||||
|         track.wayPoints.forEach { wayPoint -> | ||||
|             gpxString.appendLine("\t\t\t<trkpt lat=\"${wayPoint.latitude}\" lon=\"${wayPoint.longitude}\">") | ||||
|             gpxString.appendLine("\t\t\t\t<ele>${wayPoint.altitude}</ele>") | ||||
|             gpxString.appendLine("\t\t\t\t<time>${dateFormat.format(Date(wayPoint.time))}</time>") | ||||
|             gpxString.appendLine("\t\t\t\t<sat>${wayPoint.numberSatellites}</sat>") | ||||
|             gpxString.appendLine("\t\t\t</trkpt>") | ||||
|         } | ||||
|         gpxString.appendLine("\t\t</trkseg>") | ||||
|         gpxString.appendLine("\t</trk>") | ||||
|         gpxString.appendLine("</gpx>") | ||||
| 
 | ||||
|         return gpxString.toString() | ||||
|     } | ||||
| 
 | ||||
|     /* Toggles starred flag for given position */ | ||||
|     fun toggleStarred(context: Context, track: Track, latitude: Double, longitude: Double): Track { | ||||
|     fun toggle_waypoint_starred(context: Context, track: Track, latitude: Double, longitude: Double) | ||||
|     { | ||||
|         track.wayPoints.forEach { waypoint -> | ||||
|             if (waypoint.latitude == latitude && waypoint.longitude == longitude) { | ||||
|                 waypoint.starred = !waypoint.starred | ||||
|  | @ -167,6 +48,5 @@ object TrackHelper { | |||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return track | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -30,7 +30,6 @@ import androidx.core.content.ContextCompat | |||
| import androidx.fragment.app.Fragment | ||||
| import androidx.recyclerview.widget.DiffUtil | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import java.util.* | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.suspendCoroutine | ||||
| import kotlinx.coroutines.* | ||||
|  | @ -38,8 +37,9 @@ 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.Track | ||||
| import org.y20k.trackbook.core.Tracklist | ||||
| import org.y20k.trackbook.core.TracklistElement | ||||
| import org.y20k.trackbook.core.load_tracklist | ||||
| import org.y20k.trackbook.helpers.* | ||||
| 
 | ||||
| 
 | ||||
|  | @ -61,24 +61,20 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re | |||
| 
 | ||||
|     /* Listener Interface */ | ||||
|     interface TracklistAdapterListener { | ||||
|         fun onTrackElementTapped(tracklistElement: TracklistElement) {  } | ||||
|         // fun onTrackElementStarred(trackId: Long, starred: Boolean) | ||||
|         fun onTrackElementTapped(track: Track) {  } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Overrides onAttachedToRecyclerView from RecyclerView.Adapter */ | ||||
|     override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { | ||||
|         // get reference to listener | ||||
|     override fun onAttachedToRecyclerView(recyclerView: RecyclerView) | ||||
|     { | ||||
|         tracklistListener = fragment as TracklistAdapterListener | ||||
|         // load tracklist | ||||
|         tracklist = FileHelper.readTracklist(context) | ||||
|         tracklist.tracklistElements.sortByDescending { tracklistElement -> tracklistElement.date  } | ||||
|         tracklist = load_tracklist(context) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Overrides onCreateViewHolder from RecyclerView.Adapter */ | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { | ||||
| 
 | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder | ||||
|     { | ||||
|         when (viewType) { | ||||
|             Keys.VIEW_TYPE_STATISTICS -> { | ||||
|                 val v = LayoutInflater.from(parent.context).inflate(R.layout.element_statistics, parent, false) | ||||
|  | @ -104,16 +100,16 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re | |||
| 
 | ||||
|     /* Overrides getItemCount from RecyclerView.Adapter */ | ||||
|     override fun getItemCount(): Int { | ||||
|         // +1 ==> the total statistics element | ||||
|         return tracklist.tracklistElements.size + 1 | ||||
|         // +1 because of the total statistics element | ||||
|         return tracklist.tracks.size + 1 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Overrides onBindViewHolder from RecyclerView.Adapter */ | ||||
|     override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { | ||||
| 
 | ||||
|         when (holder) { | ||||
| 
 | ||||
|     override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) | ||||
|     { | ||||
|         when (holder) | ||||
|         { | ||||
|             // CASE STATISTICS ELEMENT | ||||
|             is ElementStatisticsViewHolder -> { | ||||
|                 val elementStatisticsViewHolder: ElementStatisticsViewHolder = holder as ElementStatisticsViewHolder | ||||
|  | @ -124,14 +120,14 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re | |||
|             is ElementTrackViewHolder -> { | ||||
|                 val positionInTracklist: Int = position - 1 // Element 0 is the statistics element. | ||||
|                 val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder | ||||
|                 elementTrackViewHolder.trackNameView.text = tracklist.tracklistElements[positionInTracklist].name | ||||
|                 elementTrackViewHolder.trackNameView.text = tracklist.tracks[positionInTracklist].name | ||||
|                 elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist) | ||||
|                 when (tracklist.tracklistElements[positionInTracklist].starred) { | ||||
|                 when (tracklist.tracks[positionInTracklist].starred) { | ||||
|                     true -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp)) | ||||
|                     false -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp)) | ||||
|                 } | ||||
|                 elementTrackViewHolder.trackElement.setOnClickListener { | ||||
|                     tracklistListener.onTrackElementTapped(tracklist.tracklistElements[positionInTracklist]) | ||||
|                     tracklistListener.onTrackElementTapped(tracklist.tracks[positionInTracklist]) | ||||
|                 } | ||||
|                 elementTrackViewHolder.starButton.setOnClickListener { | ||||
|                     toggleStarred(it, positionInTracklist) | ||||
|  | @ -144,102 +140,76 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re | |||
| 
 | ||||
| 
 | ||||
|     /* Get track name for given position */ | ||||
|     fun getTrackName(positionInRecyclerView: Int): String { | ||||
|         // first position is always the statistics element | ||||
|         return tracklist.tracklistElements[positionInRecyclerView - 1].name | ||||
|     fun getTrackName(positionInRecyclerView: Int): String | ||||
|     { | ||||
|         // Minus 1 because first position is always the statistics element | ||||
|         return tracklist.tracks[positionInRecyclerView - 1].name | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Removes track and track files for given position - used by TracklistFragment */ | ||||
|     fun removeTrackAtPosition(context: Context, position: Int) { | ||||
|     fun delete_track_at_position(context: Context, ui_index: Int) | ||||
|     { | ||||
|         CoroutineScope(IO).launch { | ||||
|             val index = position - 1 // position 0 is the statistics element | ||||
|             val tracklist_element = tracklist.tracklistElements[index] | ||||
|             val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, tracklist_element, tracklist) } | ||||
|             val track_index = ui_index - 1 // position 0 is the statistics element | ||||
|             val track = tracklist.tracks[track_index] | ||||
|             val deferred: Deferred<Unit> = async { track.delete_suspended(context) } | ||||
|             // wait for result and store in tracklist | ||||
|             withContext(Main) { | ||||
|                 tracklist = deferred.await() | ||||
|                 deferred.await() | ||||
|                 tracklist.tracks.remove(track) | ||||
|                 notifyItemChanged(0) | ||||
|                 notifyItemRemoved(position) | ||||
|                 notifyItemRangeChanged(position, tracklist.tracklistElements.size) | ||||
|                 notifyItemRemoved(ui_index) | ||||
|                 notifyItemRangeChanged(ui_index, tracklist.tracks.size) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Suspend function: Wrapper for removeTrackAtPosition */ | ||||
|     suspend fun removeTrackAtPositionSuspended(context: Context, position: Int) { | ||||
|     suspend fun delete_track_at_position_suspended(context: Context, position: Int) { | ||||
|         return suspendCoroutine { cont -> | ||||
|             cont.resume(removeTrackAtPosition(context, position)) | ||||
|             cont.resume(delete_track_at_position(context, position)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /* Removes track and track files for given track id - used by TracklistFragment */ | ||||
|     fun removeTrackById(context: Context, trackId: Long) { | ||||
|     fun delete_track_by_id(context: Context, trackId: Long) { | ||||
|         CoroutineScope(IO).launch { | ||||
|             // reload tracklist //todo check if necessary | ||||
|             tracklist = FileHelper.readTracklist(context) | ||||
|             val index: Int = tracklist.tracklistElements.indexOfFirst {it.id == trackId} | ||||
|             val index: Int = tracklist.tracks.indexOfFirst {it.id == trackId} | ||||
|             if (index == -1) { | ||||
|                 return@launch | ||||
|             } | ||||
|             val tracklist_element = tracklist.tracklistElements[index] | ||||
|             val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, tracklist_element, tracklist) } | ||||
|             // wait for result and store in tracklist | ||||
|             val position = index + 1 // position 0 is the statistics element | ||||
|             withContext(Main) { | ||||
|                 tracklist = deferred.await() | ||||
|                 notifyItemChanged(0) | ||||
|                 notifyItemRemoved(position) | ||||
|                 notifyItemRangeChanged(position, tracklist.tracklistElements.size) | ||||
|             } | ||||
|             delete_track_at_position(context, index + 1) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Returns if the adapter is empty */ | ||||
|     fun isEmpty(): Boolean { | ||||
|         return tracklist.tracklistElements.size == 0 | ||||
|         return tracklist.tracks.size == 0 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Finds current position of track element in adapter list */ | ||||
|     private fun findPosition(trackId: Long): Int { | ||||
|         tracklist.tracklistElements.forEachIndexed {index, tracklistElement -> | ||||
|             if (tracklistElement.id == trackId) | ||||
|             { | ||||
|                 return index | ||||
|             } | ||||
|         } | ||||
|         return -1 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Toggles the starred state of tracklist element - and saves tracklist */ | ||||
|     private fun toggleStarred(view: View, position: Int) { | ||||
|         val starButton: ImageButton = view as ImageButton | ||||
|         when (tracklist.tracklistElements[position].starred) { | ||||
|             true -> { | ||||
|                 starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp)) | ||||
|                 tracklist.tracklistElements[position].starred = false | ||||
|             } | ||||
|             false -> { | ||||
|                 starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp)) | ||||
|                 tracklist.tracklistElements[position].starred = true | ||||
|             } | ||||
|         if (tracklist.tracks[position].starred) | ||||
|         { | ||||
|             starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp)) | ||||
|             tracklist.tracks[position].starred = false | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp)) | ||||
|             tracklist.tracks[position].starred = true | ||||
|         } | ||||
|         tracklist.tracks[position].save_json(context) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Creates the track data string */ | ||||
|     private fun createTrackDataString(position: Int): String { | ||||
|         val tracklistElement: TracklistElement = tracklist.tracklistElements[position] | ||||
|         val track_duration_string = DateTimeHelper.convertToReadableTime(context, tracklistElement.duration) | ||||
|         val track: Track = tracklist.tracks[position] | ||||
|         val track_duration_string = DateTimeHelper.convertToReadableTime(context, track.duration) | ||||
|         val trackDataString: String | ||||
|         when (tracklistElement.name == tracklistElement.dateString) { | ||||
|         when (track.name == track.dateString) { | ||||
|             // CASE: no individual name set - exclude date | ||||
|             true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(tracklistElement.distance, useImperial)} • ${track_duration_string}" | ||||
|             true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}" | ||||
|             // CASE: no individual name set - include date | ||||
|             false -> trackDataString = "${tracklistElement.dateString} • ${LengthUnitHelper.convertDistanceToString(tracklistElement.distance, useImperial)} • ${track_duration_string}" | ||||
|             false -> trackDataString = "${track.dateString} • ${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}" | ||||
|         } | ||||
|         return trackDataString | ||||
|     } | ||||
|  | @ -251,22 +221,22 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re | |||
|     private inner class DiffCallback(val oldList: Tracklist, val newList: Tracklist): DiffUtil.Callback() { | ||||
| 
 | ||||
|         override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { | ||||
|             val oldItem = oldList.tracklistElements[oldItemPosition] | ||||
|             val newItem = newList.tracklistElements[newItemPosition] | ||||
|             val oldItem = oldList.tracks[oldItemPosition] | ||||
|             val newItem = newList.tracks[newItemPosition] | ||||
|             return oldItem.id == newItem.id | ||||
|         } | ||||
| 
 | ||||
|         override fun getOldListSize(): Int { | ||||
|             return oldList.tracklistElements.size | ||||
|             return oldList.tracks.size | ||||
|         } | ||||
| 
 | ||||
|         override fun getNewListSize(): Int { | ||||
|             return newList.tracklistElements.size | ||||
|             return newList.tracks.size | ||||
|         } | ||||
| 
 | ||||
|         override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { | ||||
|             val oldItem = oldList.tracklistElements[oldItemPosition] | ||||
|             val newItem = newList.tracklistElements[newItemPosition] | ||||
|             val oldItem = oldList.tracks[oldItemPosition] | ||||
|             val newItem = newList.tracks[newItemPosition] | ||||
|             return (oldItem.id == newItem.id) && (oldItem.distance == newItem.distance) | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -187,7 +187,7 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m | |||
|             mapView.overlays.add(trackSpecialMarkersOverlay) | ||||
|         } | ||||
|         // save track | ||||
|         CoroutineScope(Dispatchers.IO).launch { FileHelper.saveTrackSuspended(track, true) } | ||||
|         CoroutineScope(Dispatchers.IO).launch { track.save_both_suspended(context) } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -196,7 +196,7 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m | |||
|     { | ||||
|         if (track.latitude != 0.0 && track.longitude != 0.0) | ||||
|         { | ||||
|             CoroutineScope(Dispatchers.IO).launch { FileHelper.saveTrackSuspended(track, false) } | ||||
|             CoroutineScope(Dispatchers.IO).launch { track.save_json_suspended(context) } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue