Was experimenting with automatic GPX export. Hardcoded path.
These changes are several months old, I am just now committing them because I want to move on to a different experiment.
This commit is contained in:
		
							parent
							
								
									edcb149ac7
								
							
						
					
					
						commit
						7956f44ce4
					
				
					 15 changed files with 204 additions and 120 deletions
				
			
		|  | @ -2,6 +2,8 @@ | |||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     package="org.y20k.trackbook"> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     <!-- USE GPS AND NETWORK - EXCLUDE NON-GPS DEVICES --> | ||||
|     <uses-feature android:name="android.hardware.location.gps" android:required="true" /> | ||||
|     <uses-feature android:name="android.hardware.location.network" /> | ||||
|  | @ -17,13 +19,15 @@ | |||
|     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||||
|     <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" /> | ||||
| 
 | ||||
|     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|     <application | ||||
|         android:name=".Trackbook" | ||||
|         android:allowBackup="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:supportsRtl="true" | ||||
|         android:requestLegacyExternalStorage="true" | ||||
|         android:theme="@style/AppTheme"> | ||||
| 
 | ||||
|         <!-- MAIN ACTIVITY --> | ||||
|  |  | |||
|  | @ -57,6 +57,7 @@ object Keys { | |||
|     const val PREF_USE_IMPERIAL_UNITS: String = "prefUseImperialUnits" | ||||
|     const val PREF_GPS_ONLY: String = "prefGpsOnly" | ||||
|     const val PREF_OMIT_RESTS: String = "prefOmitRests" | ||||
|     const val PREF_AUTO_EXPORT_INTERVAL: String = "prefAutoExportInterval" | ||||
|     const val PREF_ALTITUDE_SMOOTHING_VALUE: String = "prefAltitudeSmoothingValue" | ||||
|     const val PREF_LOCATION_ACCURACY_THRESHOLD: String = "prefLocationAccuracyThreshold" | ||||
|     const val PREF_LOCATION_AGE_THRESHOLD: String = "prefLocationAgeThreshold" | ||||
|  | @ -99,22 +100,25 @@ object Keys { | |||
|     // default values | ||||
|     val DEFAULT_DATE: Date = Date(0L) | ||||
|     const val DEFAULT_RFC2822_DATE: String = "Thu, 01 Jan 1970 01:00:00 +0100"  // --> Date(0) | ||||
|     const val ONE_HOUR_IN_MILLISECONDS: Int = 3600000 | ||||
|     const val ONE_SECOND_IN_MILLISECONDS: Long = 1000 | ||||
|     const val ONE_MINUTE_IN_MILLISECONDS: Long = 60 * ONE_SECOND_IN_MILLISECONDS | ||||
|     const val ONE_HOUR_IN_MILLISECONDS: Long = 60 * ONE_MINUTE_IN_MILLISECONDS | ||||
|     const val EMPTY_STRING_RESOURCE: Int = 0 | ||||
|     const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1000L                   // 1 second in milliseconds | ||||
|     const val ADD_WAYPOINT_TO_TRACK_INTERVAL: Long = 1000L                      // 1 second in milliseconds | ||||
|     const val SAVE_TEMP_TRACK_INTERVAL: Long = 9000L                            // 9 seconds in milliseconds | ||||
|     const val SIGNIFICANT_TIME_DIFFERENCE: Long = 120000L                       // 2 minutes in milliseconds | ||||
|     const val STOP_OVER_THRESHOLD: Long = 300000L                               // 5 minutes in milliseconds | ||||
|     const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS | ||||
|     const val ADD_WAYPOINT_TO_TRACK_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS | ||||
|     const val SAVE_TEMP_TRACK_INTERVAL: Long = 30 * ONE_SECOND_IN_MILLISECONDS | ||||
|     const val SIGNIFICANT_TIME_DIFFERENCE: Long = 2 * ONE_MINUTE_IN_MILLISECONDS | ||||
|     const val STOP_OVER_THRESHOLD: Long = 5 * ONE_MINUTE_IN_MILLISECONDS | ||||
|     const val IMPLAUSIBLE_TRACK_START_SPEED: Double = 250.0                     // 250 km/h | ||||
|     const val DEFAULT_LATITUDE: Double = 71.172500                              // latitude Nordkapp, Norway | ||||
|     const val DEFAULT_LONGITUDE: Double = 25.784444                             // longitude Nordkapp, Norway | ||||
|     const val DEFAULT_ACCURACY: Float = 300f                                    // in meters | ||||
|     const val DEFAULT_ALTITUDE: Double = 0.0 | ||||
|     const val DEFAULT_TIME: Long = 0L | ||||
|     const val DEFAULT_AUTO_EXPORT_INTERVAL: Int = 24 | ||||
|     const val DEFAULT_ALTITUDE_SMOOTHING_VALUE: Int = 13 | ||||
|     const val DEFAULT_THRESHOLD_LOCATION_ACCURACY: Int = 30                     // 30 meters | ||||
|     const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 60000000000L               // one minute in nanoseconds | ||||
|     const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 60_000_000_000L               // one minute in nanoseconds | ||||
|     const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f                           // 15 meters | ||||
|     const val DEFAULT_ZOOM_LEVEL: Double = 16.0 | ||||
|     const val MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION: Int = 5 | ||||
|  |  | |||
|  | @ -17,12 +17,16 @@ | |||
| 
 | ||||
| package org.y20k.trackbook | ||||
| 
 | ||||
| import android.Manifest | ||||
| import android.app.Activity | ||||
| import android.content.SharedPreferences | ||||
| import android.content.pm.PackageManager | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import android.os.StrictMode | ||||
| import android.os.StrictMode.VmPolicy | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.app.ActivityCompat | ||||
| import androidx.navigation.fragment.NavHostFragment | ||||
| import androidx.navigation.ui.setupWithNavController | ||||
| import com.google.android.material.bottomnavigation.BottomNavigationView | ||||
|  | @ -31,6 +35,35 @@ import org.y20k.trackbook.helpers.AppThemeHelper | |||
| import org.y20k.trackbook.helpers.LogHelper | ||||
| import org.y20k.trackbook.helpers.PreferencesHelper | ||||
| 
 | ||||
| private const val REQUEST_EXTERNAL_STORAGE = 1 | ||||
| private val PERMISSIONS_STORAGE = arrayOf<String>( | ||||
|     Manifest.permission.READ_EXTERNAL_STORAGE, | ||||
|     Manifest.permission.WRITE_EXTERNAL_STORAGE | ||||
| ) | ||||
| 
 | ||||
| /** | ||||
|  * Checks if the app has permission to write to device storage | ||||
|  * | ||||
|  * If the app does not has permission then the user will be prompted to grant permissions | ||||
|  * | ||||
|  * @param activity | ||||
|  */ | ||||
| fun verifyStoragePermissions(activity: Activity?) | ||||
| { | ||||
|     // Check if we have write permission | ||||
|     val permission = ActivityCompat.checkSelfPermission(activity!!, | ||||
|         Manifest.permission.WRITE_EXTERNAL_STORAGE) | ||||
|     if (permission != PackageManager.PERMISSION_GRANTED) | ||||
|     { | ||||
|         // We don't have permission so prompt the user | ||||
|         ActivityCompat.requestPermissions( | ||||
|             activity, | ||||
|             PERMISSIONS_STORAGE, | ||||
|             REQUEST_EXTERNAL_STORAGE | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * MainActivity class | ||||
|  */ | ||||
|  | @ -48,7 +81,7 @@ class MainActivity : AppCompatActivity() { | |||
|     /* Overrides onCreate from AppCompatActivity */ | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
| 
 | ||||
|         verifyStoragePermissions(this) | ||||
|         // todo: remove after testing finished | ||||
|         if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | ||||
|             StrictMode.setVmPolicy( | ||||
|  |  | |||
|  | @ -275,7 +275,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe | |||
|     private fun handleTrackingManagementMenu() { | ||||
|         when (trackingState) { | ||||
|             Keys.STATE_TRACKING_PAUSED -> resumeTracking() | ||||
|             Keys.STATE_TRACKING_ACTIVE -> trackerService.stopTracking() | ||||
|             Keys.STATE_TRACKING_ACTIVE -> trackerService.pauseTracking() | ||||
|             Keys.STATE_TRACKING_NOT_STARTED -> startTracking() | ||||
|         } | ||||
|     } | ||||
|  | @ -296,9 +296,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe | |||
|         else | ||||
|         { | ||||
|             CoroutineScope(IO).launch { | ||||
|                 track.save_json(activity as Context) | ||||
|                 track.save_gpx(activity as Context) | ||||
|                 trackerService.clearTrack() | ||||
|                 trackerService.saveTrackAndClear(activity as Context) | ||||
|                 withContext(Main) { | ||||
|                     // step 4: open track in TrackFragement | ||||
|                     openTrack(track) | ||||
|  |  | |||
|  | @ -123,6 +123,16 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList | |||
|         preferenceOmitRests.summaryOff = getString(R.string.pref_omit_rests_off) | ||||
|         preferenceOmitRests.setDefaultValue(DEFAULT_OMIT_RESTS) | ||||
| 
 | ||||
|         val preferenceAutoExportInterval: SeekBarPreference = SeekBarPreference(activity as Context) | ||||
|         preferenceAutoExportInterval.title = getString(R.string.pref_auto_export_interval_title) | ||||
|         preferenceAutoExportInterval.setIcon(R.drawable.ic_bar_chart_24) | ||||
|         preferenceAutoExportInterval.key = Keys.PREF_AUTO_EXPORT_INTERVAL | ||||
|         preferenceAutoExportInterval.summary = getString(R.string.pref_auto_export_interval_summary) | ||||
|         preferenceAutoExportInterval.showSeekBarValue = true | ||||
|         preferenceAutoExportInterval.min = 1 | ||||
|         preferenceAutoExportInterval.max = 24 | ||||
|         preferenceAutoExportInterval.setDefaultValue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE) | ||||
| 
 | ||||
|         // set up "Altitude Smoothing" preference | ||||
| //        val preferenceAltitudeSmoothingValue: SeekBarPreference = SeekBarPreference(activity as Context) | ||||
| //        preferenceAltitudeSmoothingValue.title = getString(R.string.pref_altitude_smoothing_value_title) | ||||
|  | @ -190,6 +200,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList | |||
|         screen.addPreference(preferenceCategoryAdvanced) | ||||
|         screen.addPreference(preferenceOmitRests) | ||||
| //        screen.addPreference(preferenceAltitudeSmoothingValue) | ||||
|         screen.addPreference(preferenceAutoExportInterval) | ||||
|         screen.addPreference(preferenceResetAdvanced) | ||||
|         screen.addPreference(preferenceCategoryAbout) | ||||
|         screen.addPreference(preferenceAppVersion) | ||||
|  |  | |||
|  | @ -49,7 +49,7 @@ import org.y20k.trackbook.helpers.LogHelper | |||
| import org.y20k.trackbook.helpers.MapOverlayHelper | ||||
| import org.y20k.trackbook.helpers.TrackHelper | ||||
| import org.y20k.trackbook.ui.TrackFragmentLayoutHolder | ||||
| 
 | ||||
| import java.io.File | ||||
| 
 | ||||
| class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener, MapOverlayHelper.MarkerListener { | ||||
| 
 | ||||
|  | @ -138,6 +138,7 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
|         if (result.resultCode == Activity.RESULT_OK && result.data != null) | ||||
|         { | ||||
|             val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri() | ||||
|             Toast.makeText(activity as Context, sourceUri.toString(), Toast.LENGTH_LONG).show() | ||||
|             val targetUri: Uri? = result.data?.data | ||||
|             if (targetUri != null) | ||||
|             { | ||||
|  | @ -145,7 +146,8 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
|                 CoroutineScope(Dispatchers.IO).launch { | ||||
|                     FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri) | ||||
|                 } | ||||
|                 Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show() | ||||
|                 Toast.makeText(activity as Context, targetUri.toString(), Toast.LENGTH_LONG).show() | ||||
|                 // Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -211,6 +213,16 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi | |||
|             LogHelper.e(TAG, "Unable to save GPX.") | ||||
|             Toast.makeText(activity as Context, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show() | ||||
|         } | ||||
|         // val context = this.activity as Context | ||||
|         // val export_name: String = DateTimeHelper.convertToSortableDateString(layout.track.recordingStart) + Keys.GPX_FILE_EXTENSION | ||||
|         // val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri() | ||||
|         // // val targetUri: Uri = "file:///storage/emulated/0/Syncthing/GPX".toUri() | ||||
|         // val targetUri: Uri = File(File("/storage/emulated/0/Syncthing/GPX"), export_name).toUri() | ||||
|         // Toast.makeText(activity as Context, targetUri.toString(), Toast.LENGTH_LONG).show() | ||||
|         // CoroutineScope(Dispatchers.IO).launch { | ||||
|         //     FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri) | ||||
|         // } | ||||
|         // Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -58,12 +58,11 @@ class TrackerService: Service(), SensorEventListener | |||
|     var useImperial: Boolean = false | ||||
|     var gpsOnly: Boolean = false | ||||
|     var omitRests: Boolean = true | ||||
|     var autoExportInterval: Int = Keys.DEFAULT_AUTO_EXPORT_INTERVAL | ||||
|     var currentBestLocation: Location = LocationHelper.getDefaultLocation() | ||||
|     var lastSave: Date = Keys.DEFAULT_DATE | ||||
|     var lastTempSave: Date = Keys.DEFAULT_DATE | ||||
|     var lastAutoExport: 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 | ||||
|     var networkLocationListenerRegistered: Boolean = false | ||||
|  | @ -152,7 +151,7 @@ class TrackerService: Service(), SensorEventListener | |||
|     fun clearTrack() | ||||
|     { | ||||
|         track = Track() | ||||
|         resumed = false | ||||
|         stepCountOffset = 0f | ||||
|         FileHelper.delete_temp_file(this as Context) | ||||
|         trackingState = Keys.STATE_TRACKING_NOT_STARTED | ||||
|         PreferencesHelper.saveTrackingState(trackingState) | ||||
|  | @ -237,6 +236,7 @@ class TrackerService: Service(), SensorEventListener | |||
|         gpsOnly = PreferencesHelper.loadGpsOnly() | ||||
|         useImperial = PreferencesHelper.loadUseImperialUnits() | ||||
|         omitRests = PreferencesHelper.loadOmitRests() | ||||
|         autoExportInterval = PreferencesHelper.loadAutoExportInterval() | ||||
| 
 | ||||
|         locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager | ||||
|         sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager | ||||
|  | @ -259,7 +259,7 @@ class TrackerService: Service(), SensorEventListener | |||
|         LogHelper.i(TAG, "onDestroy called.") | ||||
|         if (trackingState == Keys.STATE_TRACKING_ACTIVE) | ||||
|         { | ||||
|             stopTracking() | ||||
|             pauseTracking() | ||||
|         } | ||||
|         stopForeground(true) | ||||
|         notificationManager.cancel(Keys.TRACKER_SERVICE_NOTIFICATION_ID) // this call was not necessary prior to Android 12 | ||||
|  | @ -305,7 +305,7 @@ class TrackerService: Service(), SensorEventListener | |||
|         } | ||||
|         else if (intent.action == Keys.ACTION_STOP) | ||||
|         { | ||||
|             stopTracking() | ||||
|             pauseTracking() | ||||
|         } | ||||
|         else if (intent.action == Keys.ACTION_START) | ||||
|         { | ||||
|  | @ -363,17 +363,31 @@ class TrackerService: Service(), SensorEventListener | |||
|     { | ||||
|         // 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 | ||||
|         if (track.wayPoints.isNotEmpty()) { | ||||
|             track.wayPoints.last().isStopOver = true | ||||
|         } | ||||
|         resumed = true | ||||
|         // calculate length of recording break | ||||
|         track.recordingPaused += TrackHelper.calculateDurationOfPause(track.recordingStop) | ||||
|         track.resumed = true | ||||
|         track.recordingPaused += (GregorianCalendar.getInstance().time.time - track.recordingStop.time) | ||||
|         startTracking(newTrack = false) | ||||
|     } | ||||
| 
 | ||||
|     fun saveTrackAndClear(context: Context) | ||||
|     { | ||||
|         this.pauseTracking() | ||||
|         track.save_all_files(context) | ||||
|         this.clearTrack() | ||||
|     } | ||||
| 
 | ||||
|     fun saveTrackAndStartNew(context: Context) | ||||
|     { | ||||
|         if (track.wayPoints.isNotEmpty()) | ||||
|         { | ||||
|             track.save_all_files(context) | ||||
|         } | ||||
|         track = Track() | ||||
|         FileHelper.delete_temp_file(this as Context) | ||||
|     } | ||||
| 
 | ||||
|     private fun startStepCounter() | ||||
|     { | ||||
|         val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI) | ||||
|  | @ -390,7 +404,6 @@ class TrackerService: Service(), SensorEventListener | |||
|         // set up new track | ||||
|         if (newTrack) { | ||||
|             track = Track() | ||||
|             resumed = false | ||||
|             stepCountOffset = 0f | ||||
|         } | ||||
|         trackingState = Keys.STATE_TRACKING_ACTIVE | ||||
|  | @ -400,11 +413,10 @@ class TrackerService: Service(), SensorEventListener | |||
|         startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification()) | ||||
|     } | ||||
| 
 | ||||
|     fun stopTracking() | ||||
|     fun pauseTracking() | ||||
|     { | ||||
|         track.recordingStop = GregorianCalendar.getInstance().time | ||||
|         val context: Context = this | ||||
|         CoroutineScope(IO).launch { track.save_temp_suspended(context) } | ||||
|         CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) } | ||||
| 
 | ||||
|         trackingState = Keys.STATE_TRACKING_PAUSED | ||||
|         PreferencesHelper.saveTrackingState(trackingState) | ||||
|  | @ -439,6 +451,9 @@ class TrackerService: Service(), SensorEventListener | |||
|             Keys.PREF_OMIT_RESTS -> { | ||||
|                 omitRests = PreferencesHelper.loadOmitRests() | ||||
|             } | ||||
|             Keys.PREF_AUTO_EXPORT_INTERVAL -> { | ||||
|                 autoExportInterval = PreferencesHelper.loadAutoExportInterval() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     /* | ||||
|  | @ -462,9 +477,10 @@ class TrackerService: Service(), SensorEventListener | |||
|     { | ||||
|         override fun run() { | ||||
|             // add waypoint to track - step count is continuously updated in onSensorChanged | ||||
|             val success = track.add_waypoint(currentBestLocation, omitRests, resumed) | ||||
|             val success = track.add_waypoint(currentBestLocation, omitRests, track.resumed) | ||||
|             val now: Date = GregorianCalendar.getInstance().time | ||||
|             if (success) { | ||||
|                 resumed = false | ||||
|                 track.resumed = false | ||||
| 
 | ||||
|                 // store previous smoothed altitude | ||||
|                 val previousAltitude: Double = altitudeValues.getAverage() | ||||
|  | @ -488,11 +504,14 @@ class TrackerService: Service(), SensorEventListener | |||
|                 } | ||||
| 
 | ||||
|                 // save a temp track | ||||
|                 val now: Date = GregorianCalendar.getInstance().time | ||||
|                 if (now.time - lastSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) { | ||||
|                     lastSave = now | ||||
|                 if (now.time - lastTempSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) { | ||||
|                     lastTempSave = now | ||||
|                     CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) } | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             if (now.time - track.recordingStart.time > (autoExportInterval * Keys.ONE_HOUR_IN_MILLISECONDS)) { | ||||
|                 saveTrackAndStartNew(this@TrackerService) | ||||
|             } | ||||
|             // update notification | ||||
|             displayNotification() | ||||
|  |  | |||
|  | @ -16,23 +16,36 @@ | |||
| 
 | ||||
| package org.y20k.trackbook.core | ||||
| 
 | ||||
| import android.Manifest | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.content.pm.PackageManager | ||||
| import android.location.Location | ||||
| import android.net.Uri | ||||
| import android.os.Handler | ||||
| import android.os.Looper | ||||
| import android.os.Parcelable | ||||
| import android.util.Log | ||||
| import android.widget.Toast | ||||
| import androidx.annotation.Keep | ||||
| import androidx.core.app.ActivityCompat | ||||
| import androidx.core.net.toUri | ||||
| import com.google.gson.annotations.Expose | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.parcelize.Parcelize | ||||
| import org.y20k.trackbook.Keys | ||||
| import org.y20k.trackbook.R | ||||
| import org.y20k.trackbook.helpers.DateTimeHelper | ||||
| import org.y20k.trackbook.helpers.FileHelper | ||||
| import org.y20k.trackbook.helpers.LocationHelper | ||||
| import 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 | ||||
|  | @ -50,6 +63,9 @@ data class Track ( | |||
|     @Expose var recordingStart: Date = GregorianCalendar.getInstance().time, | ||||
|     @Expose var dateString: String = DateTimeHelper.convertToReadableDate(recordingStart), | ||||
|     @Expose var recordingStop: Date = recordingStart, | ||||
|     // The resumed flag will be true for the first point that is received after unpausing a | ||||
|     // recording, so that the distance travelled while paused is not added to the track.distance. | ||||
|     @Expose var resumed: Boolean = false, | ||||
|     @Expose var maxAltitude: Double = 0.0, | ||||
|     @Expose var minAltitude: Double = 0.0, | ||||
|     @Expose var positiveElevation: Double = 0.0, | ||||
|  | @ -165,22 +181,29 @@ data class Track ( | |||
|         return File(context.getExternalFilesDir(Keys.FOLDER_GPX), basename) | ||||
|     } | ||||
| 
 | ||||
|     fun get_export_gpx_file(context: Context): File | ||||
|     { | ||||
|         val basename: String = DateTimeHelper.convertToSortableDateString(this.recordingStart) + Keys.GPX_FILE_EXTENSION | ||||
|         return File(File("/storage/emulated/0/Syncthing/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) | ||||
|     fun save_all_files(context: Context) | ||||
|     { | ||||
|         this.save_json(context) | ||||
|         this.save_gpx(context) | ||||
|         this.save_export_gpx(context) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun save_both_suspended(context: Context) | ||||
|     suspend fun save_all_files_suspended(context: Context) | ||||
|     { | ||||
|         return suspendCoroutine { cont -> | ||||
|             cont.resume(this.save_both(context)) | ||||
|             cont.resume(this.save_all_files(context)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -198,6 +221,23 @@ data class Track ( | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun save_export_gpx(context: Context) | ||||
|     { | ||||
|         val gpx: String = this.to_gpx() | ||||
|         val outputfile: File = this.get_export_gpx_file(context) | ||||
|         FileHelper.write_text_file_noblank(gpx, outputfile) | ||||
|         Handler(Looper.getMainLooper()).post { | ||||
|             Toast.makeText(context, outputfile.toString(), Toast.LENGTH_SHORT).show() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     suspend fun save_export_gpx_suspended(context: Context) | ||||
|     { | ||||
|         return suspendCoroutine { cont -> | ||||
|             cont.resume(this.save_export_gpx(context)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun save_json(context: Context) | ||||
|     { | ||||
|         val json: String = this.to_json() | ||||
|  |  | |||
|  | @ -55,7 +55,8 @@ object FileHelper { | |||
|     suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) { | ||||
|         return suspendCoroutine { cont -> | ||||
|             track.name = newName | ||||
|             track.save_both(context) | ||||
|             track.save_json(context) | ||||
|             track.save_gpx(context) | ||||
|             cont.resume(Unit) | ||||
|         } | ||||
|     } | ||||
|  | @ -71,7 +72,7 @@ object FileHelper { | |||
| 
 | ||||
| 
 | ||||
|     /* Copies file to specified target */ | ||||
|     private fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) { | ||||
|     fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) { | ||||
|         val inputStream = context.contentResolver.openInputStream(originalFileUri) | ||||
|         val outputStream = context.contentResolver.openOutputStream(targetFileUri) | ||||
|         if (outputStream != null) { | ||||
|  |  | |||
|  | @ -71,9 +71,11 @@ object PreferencesHelper { | |||
|         return sharedPreferences.getBoolean(Keys.PREF_OMIT_RESTS, true) | ||||
|     } | ||||
| 
 | ||||
| //    /* Load altitude smoothing value */ | ||||
|     fun loadAutoExportInterval(): Int { | ||||
|         return sharedPreferences.getInt(Keys.PREF_AUTO_EXPORT_INTERVAL, Keys.DEFAULT_AUTO_EXPORT_INTERVAL) | ||||
|     } | ||||
| 
 | ||||
| //    fun loadAltitudeSmoothingValue(): Int { | ||||
| //        // load current setting | ||||
| //        return sharedPreferences.getInt(Keys.PREF_ALTITUDE_SMOOTHING_VALUE, Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE) | ||||
| //    } | ||||
| 
 | ||||
|  |  | |||
|  | @ -32,10 +32,6 @@ object TrackHelper { | |||
| 
 | ||||
|     /* Adds given locatiom as waypoint to track */ | ||||
| 
 | ||||
| 
 | ||||
|     /* Calculates time passed since last stop of recording */ | ||||
|     fun calculateDurationOfPause(recordingStop: Date): Long = GregorianCalendar.getInstance().time.time - recordingStop.time | ||||
| 
 | ||||
|     /* Toggles starred flag for given position */ | ||||
|     fun toggle_waypoint_starred(context: Context, track: Track, latitude: Double, longitude: Double) | ||||
|     { | ||||
|  |  | |||
|  | @ -104,7 +104,6 @@ object UiHelper { | |||
| 
 | ||||
|         override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { | ||||
|             // disable swipe for statistics element | ||||
|             if (viewHolder is TracklistAdapter.ElementStatisticsViewHolder) return 0 | ||||
|             return super.getSwipeDirs(recyclerView, viewHolder) | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -75,64 +75,39 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re | |||
|     /* Overrides onCreateViewHolder from RecyclerView.Adapter */ | ||||
|     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) | ||||
|                 return ElementStatisticsViewHolder(v) | ||||
|             } | ||||
|             else -> { | ||||
|                 val v = LayoutInflater.from(parent.context).inflate(R.layout.element_track, parent, false) | ||||
|                 return ElementTrackViewHolder(v) | ||||
|             } | ||||
|         } | ||||
|         val v = LayoutInflater.from(parent.context).inflate(R.layout.element_track, parent, false) | ||||
|         return ElementTrackViewHolder(v) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Overrides getItemViewType */ | ||||
|     override fun getItemViewType(position: Int): Int { | ||||
|         if (position == 0) { | ||||
|             return Keys.VIEW_TYPE_STATISTICS | ||||
|         } else { | ||||
|             return Keys.VIEW_TYPE_TRACK | ||||
|         } | ||||
|         return Keys.VIEW_TYPE_TRACK | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Overrides getItemCount from RecyclerView.Adapter */ | ||||
|     override fun getItemCount(): Int { | ||||
|         // +1 because of the total statistics element | ||||
|         return tracklist.tracks.size + 1 | ||||
|         return tracklist.tracks.size | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /* Overrides onBindViewHolder from RecyclerView.Adapter */ | ||||
|     override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) | ||||
|     { | ||||
|         when (holder) | ||||
|         { | ||||
|             // CASE STATISTICS ELEMENT | ||||
|             is ElementStatisticsViewHolder -> { | ||||
|                 val elementStatisticsViewHolder: ElementStatisticsViewHolder = holder | ||||
|                 elementStatisticsViewHolder.totalDistanceView.text = LengthUnitHelper.convertDistanceToString(tracklist.get_total_distance(), useImperial) | ||||
|             } | ||||
| 
 | ||||
|             // CASE TRACK ELEMENT | ||||
|             is ElementTrackViewHolder -> { | ||||
|                 val positionInTracklist: Int = position - 1 // Element 0 is the statistics element. | ||||
|                 val elementTrackViewHolder: ElementTrackViewHolder = holder | ||||
|                 elementTrackViewHolder.trackNameView.text = tracklist.tracks[positionInTracklist].name | ||||
|                 elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist) | ||||
|                 when (tracklist.tracks[positionInTracklist].starred) { | ||||
|                     true -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp)) | ||||
|                     false -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp)) | ||||
|                 } | ||||
|                 elementTrackViewHolder.trackElement.setOnClickListener { | ||||
|                     tracklistListener.onTrackElementTapped(tracklist.tracks[positionInTracklist]) | ||||
|                 } | ||||
|                 elementTrackViewHolder.starButton.setOnClickListener { | ||||
|                     toggleStarred(it, positionInTracklist) | ||||
|                 } | ||||
|             } | ||||
|         val positionInTracklist: Int = position | ||||
|         val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder | ||||
|         elementTrackViewHolder.trackNameView.text = tracklist.tracks[positionInTracklist].name | ||||
|         elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist) | ||||
|         when (tracklist.tracks[positionInTracklist].starred) { | ||||
|             true -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp)) | ||||
|             false -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp)) | ||||
|         } | ||||
|         elementTrackViewHolder.trackElement.setOnClickListener { | ||||
|             tracklistListener.onTrackElementTapped(tracklist.tracks[positionInTracklist]) | ||||
|         } | ||||
|         elementTrackViewHolder.starButton.setOnClickListener { | ||||
|             toggleStarred(it, positionInTracklist) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -140,18 +115,16 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re | |||
|     /* Get track name for given position */ | ||||
|     fun getTrackName(positionInRecyclerView: Int): String | ||||
|     { | ||||
|         // Minus 1 because first position is always the statistics element | ||||
|         return tracklist.tracks[positionInRecyclerView - 1].name | ||||
|         return tracklist.tracks[positionInRecyclerView].name | ||||
|     } | ||||
| 
 | ||||
|     fun delete_track_at_position(context: Context, ui_index: Int) | ||||
|     fun delete_track_at_position(context: Context, index: Int) | ||||
|     { | ||||
|         val track_index = ui_index - 1 // position 0 is the statistics element | ||||
|         val track = tracklist.tracks[track_index] | ||||
|         val track = tracklist.tracks[index] | ||||
|         track.delete(context) | ||||
|         tracklist.tracks.remove(track) | ||||
|         notifyItemChanged(0) | ||||
|         notifyItemRemoved(ui_index) | ||||
|         notifyItemRemoved(index) | ||||
|         notifyItemRangeChanged(index, this.itemCount); | ||||
|     } | ||||
| 
 | ||||
|     suspend fun delete_track_at_position_suspended(context: Context, position: Int) | ||||
|  | @ -167,10 +140,12 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re | |||
|         if (index == -1) { | ||||
|             return | ||||
|         } | ||||
|         delete_track_at_position(context, index + 1) | ||||
|         tracklist.tracks[index].delete(context) | ||||
|         tracklist.tracks.removeAt(index) | ||||
|         notifyItemRemoved(index) | ||||
|         notifyItemRangeChanged(index, this.itemCount); | ||||
|     } | ||||
| 
 | ||||
|     /* Returns if the adapter is empty */ | ||||
|     fun isEmpty(): Boolean { | ||||
|         return tracklist.tracks.size == 0 | ||||
|     } | ||||
|  | @ -249,16 +224,4 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re | |||
|     /* | ||||
|      * End of inner class | ||||
|      */ | ||||
| 
 | ||||
| 
 | ||||
|     /* | ||||
|      * Inner class: ViewHolder for a statistics element | ||||
|      */ | ||||
|     inner class ElementStatisticsViewHolder (elementStatisticsLayout: View): RecyclerView.ViewHolder(elementStatisticsLayout) { | ||||
|         val totalDistanceView: TextView = elementStatisticsLayout.findViewById(R.id.total_distance_data) | ||||
|     } | ||||
|     /* | ||||
|      * End of inner class | ||||
|      */ | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -187,7 +187,7 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m | |||
|             mapView.overlays.add(trackSpecialMarkersOverlay) | ||||
|         } | ||||
|         // save track | ||||
|         CoroutineScope(Dispatchers.IO).launch { track.save_both_suspended(context) } | ||||
|         CoroutineScope(Dispatchers.IO).launch { track.save_all_files_suspended(context) } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -85,6 +85,8 @@ | |||
|     <string name="pref_accuracy_threshold_title">Accuracy Threshold</string> | ||||
|     <string name="pref_altitude_smoothing_value_summary" translatable="false">Number of waypoints used to smooth the elevation curve.</string> | ||||
|     <string name="pref_altitude_smoothing_value_title" translatable="false">Altitude Smoothing</string> | ||||
|     <string name="pref_auto_export_interval_summary">Automatically export GPX file after this many hours.</string> | ||||
|     <string name="pref_auto_export_interval_title">Auto Export Interval</string> | ||||
|     <string name="pref_advanced_title">Advanced</string> | ||||
|     <string name="pref_delete_non_starred_summary">Delete all recordings in \"Tracks\" that are not starred.</string> | ||||
|     <string name="pref_delete_non_starred_title">Delete Non-Starred Recordings</string> | ||||
|  | @ -98,7 +100,7 @@ | |||
|     <string name="pref_imperial_measurement_units_title">Use Imperial Measurements</string> | ||||
|     <string name="pref_omit_rests_on">Waypoints will not be recorded if they are too close to the previous waypoint.</string> | ||||
|     <string name="pref_omit_rests_off">All waypoints will be recorded, even while standing still.</string> | ||||
|     <string name="pref_omit_rests_title">Omit points during rests</string> | ||||
|     <string name="pref_omit_rests_title">Omit repeated points</string> | ||||
|     <string name="pref_report_issue_summary">Report bugs and suggest improvements on GitHub.</string> | ||||
|     <string name="pref_report_issue_title">Report Issue</string> | ||||
|     <string name="pref_reset_advanced_summary">Reset advanced settings to defaults.</string> | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue