checkpoint

This commit is contained in:
voussoir 2023-03-09 21:34:02 -08:00
parent df77b089ac
commit 04fa76249b
7 changed files with 197 additions and 191 deletions

View file

@ -58,7 +58,7 @@ class MapFragment : Fragment()
// https://gist.github.com/Dvik/a3de88d39da9d1d6d175025a56c5e797#file-viewextension-kt and
// https://proandroiddev.com/android-full-screen-ui-with-transparent-status-bar-ef52f3adde63
// get current best location
currentBestLocation = LocationHelper.getLastKnownLocation(activity as Context)
currentBestLocation = getLastKnownLocation(activity as Context)
// get saved tracking state
trackingState = PreferencesHelper.loadTrackingState()
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

View file

@ -20,7 +20,9 @@ import YesNoDialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.preference.*
@ -118,6 +120,10 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceDeviceID.summary = getString(R.string.pref_device_id_summary) + "\n" + PreferencesHelper.load_device_id()
preferenceDeviceID.setDefaultValue(random_device_id())
preferenceCategoryGeneral.contains(preferenceDeviceID)
preferenceDeviceID.setOnPreferenceChangeListener { preference, newValue ->
preferenceDeviceID.summary = getString(R.string.pref_device_id_summary) + "\n" + newValue
return@setOnPreferenceChangeListener true
}
screen.addPreference(preferenceDeviceID)
val preferenceCategoryAbout: PreferenceCategory = PreferenceCategory(context)

View file

@ -57,7 +57,7 @@ class TrackerService: Service(), SensorEventListener
var device_id: String = random_device_id()
var recording_started: Date = GregorianCalendar.getInstance().time
var commitInterval: Int = Keys.COMMIT_INTERVAL
var currentBestLocation: Location = LocationHelper.getDefaultLocation()
var currentBestLocation: Location = getDefaultLocation()
var lastCommit: Date = Keys.DEFAULT_DATE
var stepCountOffset: Float = 0f
lateinit var track: Track
@ -74,7 +74,6 @@ class TrackerService: Service(), SensorEventListener
private lateinit var gpsLocationListener: LocationListener
private lateinit var networkLocationListener: LocationListener
/* Adds a GPS location listener to location manager */
private fun addGpsLocationListener()
{
if (gpsLocationListenerRegistered)
@ -83,7 +82,7 @@ class TrackerService: Service(), SensorEventListener
return
}
gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
gpsProviderActive = isGpsEnabled(locationManager)
if (! gpsProviderActive)
{
LogHelper.w(TAG, "Device GPS is not enabled.")
@ -107,7 +106,6 @@ class TrackerService: Service(), SensorEventListener
LogHelper.v(TAG, "Added GPS location listener.")
}
/* Adds a Network location listener to location manager */
private fun addNetworkLocationListener()
{
if (gpsOnly)
@ -122,7 +120,7 @@ class TrackerService: Service(), SensorEventListener
return
}
networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
networkProviderActive = isNetworkEnabled(locationManager)
if (!networkProviderActive)
{
LogHelper.w(TAG, "Unable to add Network location listener.")
@ -151,7 +149,7 @@ class TrackerService: Service(), SensorEventListener
return object : LocationListener {
override fun onLocationChanged(location: Location)
{
if (LocationHelper.isBetterLocation(location, currentBestLocation)) {
if (isBetterLocation(location, currentBestLocation)) {
currentBestLocation = location
}
}
@ -159,16 +157,16 @@ class TrackerService: Service(), SensorEventListener
{
LogHelper.v(TAG, "onProviderEnabled $provider")
when (provider) {
LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
LocationManager.NETWORK_PROVIDER -> networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
LocationManager.GPS_PROVIDER -> gpsProviderActive = isGpsEnabled(locationManager)
LocationManager.NETWORK_PROVIDER -> networkProviderActive = isNetworkEnabled(locationManager)
}
}
override fun onProviderDisabled(provider: String)
{
LogHelper.v(TAG, "onProviderDisabled $provider")
when (provider) {
LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
LocationManager.NETWORK_PROVIDER -> networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
LocationManager.GPS_PROVIDER -> gpsProviderActive = isGpsEnabled(locationManager)
LocationManager.NETWORK_PROVIDER -> networkProviderActive = isNetworkEnabled(locationManager)
}
}
override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?)
@ -179,7 +177,8 @@ class TrackerService: Service(), SensorEventListener
}
/* Displays or updates notification */
private fun displayNotification(): Notification {
private fun displayNotification(): Notification
{
val notification: Notification = notificationHelper.createNotification(
trackingState,
iso8601(GregorianCalendar.getInstance().time)
@ -189,7 +188,8 @@ class TrackerService: Service(), SensorEventListener
}
/* Overrides onAccuracyChanged from SensorEventListener */
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int)
{
LogHelper.v(TAG, "Accuracy changed: $accuracy")
}
@ -220,24 +220,24 @@ class TrackerService: Service(), SensorEventListener
sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationHelper = NotificationHelper(this)
gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
gpsProviderActive = isGpsEnabled(locationManager)
networkProviderActive = isNetworkEnabled(locationManager)
gpsLocationListener = createLocationListener()
networkLocationListener = createLocationListener()
trackingState = PreferencesHelper.loadTrackingState()
currentBestLocation = LocationHelper.getLastKnownLocation(this)
currentBestLocation = getLastKnownLocation(this)
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
}
/* Overrides onDestroy from Service */
override fun onDestroy() {
LogHelper.i("VOUSSOIR", "TrackerService.onDestroy.")
super.onDestroy()
LogHelper.i(TAG, "onDestroy called.")
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
{
pauseTracking()
}
stopForeground(true)
stopForeground(STOP_FOREGROUND_REMOVE)
notificationManager.cancel(Keys.TRACKER_SERVICE_NOTIFICATION_ID) // this call was not necessary prior to Android 12
PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener)
removeGpsLocationListener()
@ -316,7 +316,6 @@ class TrackerService: Service(), SensorEventListener
}
}
/* Adds location listeners to location manager */
fun removeNetworkLocationListener()
{
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
@ -365,15 +364,14 @@ class TrackerService: Service(), SensorEventListener
stopForeground(STOP_FOREGROUND_DETACH)
}
/*
* Defines the listener for changes in shared preferences
*/
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) {
when (key)
{
Keys.PREF_GPS_ONLY ->
{
gpsOnly = PreferencesHelper.loadGpsOnly()
when (gpsOnly) {
when (gpsOnly)
{
true -> removeNetworkLocationListener()
false -> addNetworkLocationListener()
}
@ -418,12 +416,12 @@ class TrackerService: Service(), SensorEventListener
Log.i("VOUSSOIR", "Omitting due to 0,0 location.")
return false
}
if (! LocationHelper.isRecentEnough(location))
if (! isRecentEnough(location))
{
Log.i("VOUSSOIR", "Omitting due to not recent enough.")
return false
}
if (! LocationHelper.isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY))
if (! isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY))
{
Log.i("VOUSSOIR", "Omitting due to not accurate enough.")
return false
@ -440,7 +438,7 @@ class TrackerService: Service(), SensorEventListener
{
return true
}
if (! LocationHelper.isDifferentEnough(track.trkpts.last().toLocation(), location, omitRests))
if (! isDifferentEnough(track.trkpts.last().toLocation(), location, omitRests))
{
Log.i("VOUSSOIR", "Omitting due to too close to previous.")
return false

View file

@ -17,7 +17,7 @@
package org.y20k.trackbook
import android.location.Location
import org.y20k.trackbook.helpers.LocationHelper
import org.y20k.trackbook.helpers.getNumberOfSatellites
import java.util.*
/*
@ -42,7 +42,7 @@ data class Trkpt(
altitude=location.altitude,
accuracy=location.accuracy,
time=location.time,
numberSatellites=LocationHelper.getNumberOfSatellites(location),
numberSatellites=getNumberOfSatellites(location),
)
/* Converts WayPoint into Location */

View file

@ -27,159 +27,163 @@ import androidx.core.content.ContextCompat
import org.y20k.trackbook.Keys
import kotlin.math.pow
/*
* Keys object
*/
object LocationHelper {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(LocationHelper::class.java)
/* Get default location */
fun getDefaultLocation(): Location {
val defaultLocation: Location = Location(LocationManager.NETWORK_PROVIDER)
defaultLocation.latitude = Keys.DEFAULT_LATITUDE
defaultLocation.longitude = Keys.DEFAULT_LONGITUDE
defaultLocation.accuracy = Keys.DEFAULT_ACCURACY
defaultLocation.altitude = Keys.DEFAULT_ALTITUDE
defaultLocation.time = Keys.DEFAULT_DATE.time
return defaultLocation
}
/* Tries to return the last location that the system has stored */
fun getLastKnownLocation(context: Context): Location {
// get last location that Trackbook has stored
var lastKnownLocation: Location = PreferencesHelper.loadCurrentBestLocation()
// try to get the last location the system has stored - it is probably more recent
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val lastKnownLocationGps: Location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) ?: lastKnownLocation
val lastKnownLocationNetwork: Location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) ?: lastKnownLocation
when (isBetterLocation(lastKnownLocationGps, lastKnownLocationNetwork)) {
true -> lastKnownLocation = lastKnownLocationGps
false -> lastKnownLocation = lastKnownLocationNetwork
}
}
return lastKnownLocation
}
/* Determines whether one location reading is better than the current location fix */
fun isBetterLocation(location: Location, currentBestLocation: Location?): Boolean
{
// Credit: https://developer.android.com/guide/topics/location/strategies.html#BestEstimate
if (currentBestLocation == null) {
// a new location is always better than no location
return true
}
// check whether the new location fix is newer or older
val timeDelta: Long = location.time - currentBestLocation.time
val isSignificantlyNewer: Boolean = timeDelta > Keys.SIGNIFICANT_TIME_DIFFERENCE
val isSignificantlyOlder:Boolean = timeDelta < -Keys.SIGNIFICANT_TIME_DIFFERENCE
when {
// if it's been more than two minutes since the current location, use the new location because the user has likely moved
isSignificantlyNewer -> return true
// if the new location is more than two minutes older, it must be worse
isSignificantlyOlder -> return false
}
// check whether the new location fix is more or less accurate
val isNewer: Boolean = timeDelta > 0L
val accuracyDelta: Float = location.accuracy - currentBestLocation.accuracy
val isLessAccurate: Boolean = accuracyDelta > 0f
val isMoreAccurate: Boolean = accuracyDelta < 0f
val isSignificantlyLessAccurate: Boolean = accuracyDelta > 200f
// check if the old and new location are from the same provider
val isFromSameProvider: Boolean = location.provider == currentBestLocation.provider
// determine location quality using a combination of timeliness and accuracy
return when {
isMoreAccurate -> true
isNewer && !isLessAccurate -> true
isNewer && !isSignificantlyLessAccurate && isFromSameProvider -> true
else -> false
}
}
/* Checks if GPS location provider is available and enabled */
fun isGpsEnabled(locationManager: LocationManager): Boolean
{
if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER))
{
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
}
else
{
return false
}
}
/* Checks if Network location provider is available and enabled */
fun isNetworkEnabled(locationManager: LocationManager): Boolean {
if (locationManager.allProviders.contains(LocationManager.NETWORK_PROVIDER)) {
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
} else {
return false
}
}
/* Checks if given location is new */
fun isRecentEnough(location: Location): Boolean {
val locationAge: Long = SystemClock.elapsedRealtimeNanos() - location.elapsedRealtimeNanos
return locationAge < Keys.DEFAULT_THRESHOLD_LOCATION_AGE
}
/* Checks if given location is accurate */
fun isAccurateEnough(location: Location, locationAccuracyThreshold: Int): Boolean {
val isAccurate: Boolean
when (location.provider) {
LocationManager.GPS_PROVIDER -> isAccurate = location.accuracy < locationAccuracyThreshold
else -> isAccurate = location.accuracy < locationAccuracyThreshold + 10 // a bit more relaxed when location comes from network provider
}
return isAccurate
}
/* Checks if given location is different enough compared to previous location */
fun isDifferentEnough(previousLocation: Location?, location: Location, omitRests: Boolean): Boolean {
// check if previous location is (not) available
if (previousLocation == null)
{
return true
}
if (! omitRests)
{
return true
}
// location.accuracy is given as 1 standard deviation, with a 68% chance
// that the true position is within a circle of this radius.
// These formulas determine if the difference between the last point and
// new point is statistically significant.
val accuracy: Float = if (location.accuracy != 0.0f) location.accuracy else Keys.DEFAULT_THRESHOLD_DISTANCE
val previousAccuracy: Float = if (previousLocation.accuracy != 0.0f) previousLocation.accuracy else Keys.DEFAULT_THRESHOLD_DISTANCE
val accuracyDelta: Double = Math.sqrt((accuracy.pow(2) + previousAccuracy.pow(2)).toDouble())
val distance: Float = previousLocation.distanceTo(location)
// With 1*accuracyDelta we have 68% confidence that the points are
// different. We can multiply this number to increase confidence but
// decrease point recording frequency if needed.
return distance > accuracyDelta
}
/* Get number of satellites from Location extras */
fun getNumberOfSatellites(location: Location): Int {
val numberOfSatellites: Int
val extras: Bundle? = location.extras
if (extras != null && extras.containsKey("satellites")) {
numberOfSatellites = extras.getInt("satellites", 0)
} else {
numberOfSatellites = 0
}
return numberOfSatellites
}
/* Get default location */
fun getDefaultLocation(): Location
{
val defaultLocation: Location = Location(LocationManager.NETWORK_PROVIDER)
defaultLocation.latitude = Keys.DEFAULT_LATITUDE
defaultLocation.longitude = Keys.DEFAULT_LONGITUDE
defaultLocation.accuracy = Keys.DEFAULT_ACCURACY
defaultLocation.altitude = Keys.DEFAULT_ALTITUDE
defaultLocation.time = Keys.DEFAULT_DATE.time
return defaultLocation
}
/* Tries to return the last location that the system has stored */
fun getLastKnownLocation(context: Context): Location
{
// get last location that Trackbook has stored
var lastKnownLocation: Location = PreferencesHelper.loadCurrentBestLocation()
// try to get the last location the system has stored - it is probably more recent
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val lastKnownLocationGps: Location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) ?: lastKnownLocation
val lastKnownLocationNetwork: Location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) ?: lastKnownLocation
when (isBetterLocation(lastKnownLocationGps, lastKnownLocationNetwork)) {
true -> lastKnownLocation = lastKnownLocationGps
false -> lastKnownLocation = lastKnownLocationNetwork
}
}
return lastKnownLocation
}
/* Determines whether one location reading is better than the current location fix */
fun isBetterLocation(location: Location, currentBestLocation: Location?): Boolean
{
// Credit: https://developer.android.com/guide/topics/location/strategies.html#BestEstimate
if (currentBestLocation == null)
{
// a new location is always better than no location
return true
}
// check whether the new location fix is newer or older
val timeDelta: Long = location.time - currentBestLocation.time
val isSignificantlyNewer: Boolean = timeDelta > Keys.SIGNIFICANT_TIME_DIFFERENCE
val isSignificantlyOlder:Boolean = timeDelta < -Keys.SIGNIFICANT_TIME_DIFFERENCE
when {
// if it's been more than two minutes since the current location, use the new location because the user has likely moved
isSignificantlyNewer -> return true
// if the new location is more than two minutes older, it must be worse
isSignificantlyOlder -> return false
}
// check whether the new location fix is more or less accurate
val isNewer: Boolean = timeDelta > 0L
val accuracyDelta: Float = location.accuracy - currentBestLocation.accuracy
val isLessAccurate: Boolean = accuracyDelta > 0f
val isMoreAccurate: Boolean = accuracyDelta < 0f
val isSignificantlyLessAccurate: Boolean = accuracyDelta > 200f
// check if the old and new location are from the same provider
val isFromSameProvider: Boolean = location.provider == currentBestLocation.provider
// determine location quality using a combination of timeliness and accuracy
return when {
isMoreAccurate -> true
isNewer && !isLessAccurate -> true
isNewer && !isSignificantlyLessAccurate && isFromSameProvider -> true
else -> false
}
}
/* Checks if GPS location provider is available and enabled */
fun isGpsEnabled(locationManager: LocationManager): Boolean
{
if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER))
{
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
}
else
{
return false
}
}
/* Checks if Network location provider is available and enabled */
fun isNetworkEnabled(locationManager: LocationManager): Boolean
{
if (locationManager.allProviders.contains(LocationManager.NETWORK_PROVIDER))
{
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
else
{
return false
}
}
/* Checks if given location is new */
fun isRecentEnough(location: Location): Boolean
{
val locationAge: Long = SystemClock.elapsedRealtimeNanos() - location.elapsedRealtimeNanos
return locationAge < Keys.DEFAULT_THRESHOLD_LOCATION_AGE
}
/* Checks if given location is accurate */
fun isAccurateEnough(location: Location, locationAccuracyThreshold: Int): Boolean
{
if (location.provider == LocationManager.GPS_PROVIDER)
{
return location.accuracy < locationAccuracyThreshold
}
else
{
return location.accuracy < locationAccuracyThreshold + 10 // a bit more relaxed when location comes from network provider
}
}
/* Checks if given location is different enough compared to previous location */
fun isDifferentEnough(previousLocation: Location?, location: Location, omitRests: Boolean): Boolean
{
// check if previous location is (not) available
if (previousLocation == null)
{
return true
}
if (! omitRests)
{
return true
}
// location.accuracy is given as 1 standard deviation, with a 68% chance
// that the true position is within a circle of this radius.
// These formulas determine if the difference between the last point and
// new point is statistically significant.
val accuracy: Float = if (location.accuracy != 0.0f) location.accuracy else Keys.DEFAULT_THRESHOLD_DISTANCE
val previousAccuracy: Float = if (previousLocation.accuracy != 0.0f) previousLocation.accuracy else Keys.DEFAULT_THRESHOLD_DISTANCE
val accuracyDelta: Double = Math.sqrt((accuracy.pow(2) + previousAccuracy.pow(2)).toDouble())
val distance: Float = previousLocation.distanceTo(location)
// With 1*accuracyDelta we have 68% confidence that the points are
// different. We can multiply this number to increase confidence but
// decrease point recording frequency if needed.
return distance > accuracyDelta
}
/* Get number of satellites from Location extras */
fun getNumberOfSatellites(location: Location): Int
{
val numberOfSatellites: Int
val extras: Bundle? = location.extras
if (extras != null && extras.containsKey("satellites")) {
numberOfSatellites = extras.getInt("satellites", 0)
} else {
numberOfSatellites = 0
}
return numberOfSatellites
}

View file

@ -134,8 +134,6 @@ fun createSpecialMakersTrackOverlay(context: Context, map_view: MapView, track:
map_view.overlays.add(createOverlay(context, overlayItems))
}
fun createOverlayItem(context: Context, latitude: Double, longitude: Double, accuracy: Float, provider: String, time: Long): OverlayItem
{
val title: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(time)}"

View file

@ -65,7 +65,6 @@ data class MapFragmentLayoutHolder(
private val trackingState: Int
)
{
/* Main class variables */
val rootView: View
var userInteraction: Boolean = false
val currentLocationButton: FloatingActionButton
@ -75,12 +74,13 @@ data class MapFragmentLayoutHolder(
private var current_location_radius: Polygon = Polygon()
private var currentTrackOverlay: SimpleFastPointOverlay?
private var currentTrackSpecialMarkerOverlay: ItemizedIconOverlay<OverlayItem>?
private val useImperial: Boolean = PreferencesHelper.loadUseImperialUnits()
private var locationErrorBar: Snackbar
private var controller: IMapController
private var zoomLevel: Double
init {
init
{
Log.i("VOUSSOIR", "MapFragmentLayoutHolder.init")
// find views
rootView = inflater.inflate(R.layout.fragment_map, container, false)
mapView = rootView.findViewById(R.id.map)
@ -164,7 +164,7 @@ data class MapFragmentLayoutHolder(
fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_TRACKING_STOPPED)
{
Log.i("VOUSSOIR", "MapFragmentLayoutHolder.markCurrentPosition")
val locationIsOld: Boolean = !(LocationHelper.isRecentEnough(location))
val locationIsOld: Boolean = !(isRecentEnough(location))
// create marker
val newMarker: Drawable
@ -228,7 +228,7 @@ data class MapFragmentLayoutHolder(
val p = Polygon()
p.points = Polygon.pointsAsCircle(GeoPoint(homepoint.location.latitude, homepoint.location.longitude), homepoint.location.accuracy.toDouble())
p.fillPaint.color = Color.argb(64, 255, 193, 7)
p.outlinePaint.color = Color.argb(128, 255, 193, 7)
p.outlinePaint.color = Color.argb(0, 0, 0, 0)
map_view.overlays.add(p)
}
}