Rewrite TrackerService to be more like a state machine.

This commit is contained in:
voussoir 2023-04-30 15:14:05 -07:00
parent 2e5be6a4e4
commit 3710755bc1
11 changed files with 280 additions and 332 deletions

View file

@ -21,7 +21,7 @@ The goal of this fork is to make 24/7 recording easier. I want to be able to run
trkpt has three states of power management. The states transition like this:
1. **FULL POWER**: receives location updates as fast as Android provides them. A wakelock is used to prevent doze.
1. **FULL POWER**: receives location updates as fast as Android provides them. A wakelock is used to resist doze (though I think some devices will doze anyway because they do not respect the user).
Stay near homepoint for a few minutes → Sleep

View file

@ -10,8 +10,8 @@ android {
applicationId 'net.voussoir.trkpt'
minSdkVersion 25
targetSdk 32
versionCode 54
versionName '1.0.3'
versionCode 55
versionName '1.1.0'
resConfigs "en", "da", "de", "fr", "hr", "id", "it", "ja", "nb-rNO", "nl", "pl", "pt-rBR", "ru", "sv", "tr", "zh-rCN"
}

View file

@ -53,18 +53,6 @@
</intent-filter>
</service>
<!-- TRACKING TOGGLE SERVICE SYSTEM QUICK SETTINGS -->
<service
android:name="net.voussoir.trkpt.TrackingToggleTileService"
android:label="@string/quick_settings_tile_title_default"
android:icon="@drawable/ic_notification_icon_small_24dp"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<!-- FILE PROVIDER GPX -->
<provider
android:name="androidx.core.content.FileProvider"

View file

@ -40,7 +40,6 @@ object Keys {
// args
const val ARG_TRACK_TITLE: String = "ArgTrackTitle"
const val ARG_TRACK_ID: String = "ArgTrackID"
const val ARG_TRACK_DEVICE_ID: String = "ArgTrackDeviceID"
const val ARG_TRACK_START_TIME: String = "ArgTrackStartTime"
const val ARG_TRACK_STOP_TIME: String = "ArgTrackStopTime"
@ -67,12 +66,15 @@ object Keys {
const val PREF_MAX_ACCURACY: String = "prefMaxAccuracy"
// states
const val STATE_TRACKING_STOPPED: Int = 0
const val STATE_TRACKING_ACTIVE: Int = 1
const val STATE_STOP: Int = 0
const val STATE_FULL_RECORDING: Int = 1
const val STATE_ARRIVED_AT_HOME: Int = 2
const val STATE_SLEEP: Int = 3
const val STATE_DEAD: Int = 4
const val STATE_MAPVIEW: Int = 5
const val LOCATION_INTERVAL_FULL_POWER: Long = 0
const val LOCATION_INTERVAL_SLEEP: Long = ONE_MINUTE_IN_MILLISECONDS
const val LOCATION_INTERVAL_DEAD: Long = -1
const val LOCATION_INTERVAL_STOP: Long = -2
const val LOCATION_INTERVAL_STOP: Long = -1
const val STATE_THEME_FOLLOW_SYSTEM: String = "stateFollowSystem"
const val STATE_THEME_LIGHT_MODE: String = "stateLightMode"
const val STATE_THEME_DARK_MODE: String = "stateDarkMode"

View file

@ -203,9 +203,9 @@ class MapFragment : Fragment()
{
return@setOnClickListener
}
if (tracker.trackingState == Keys.STATE_TRACKING_ACTIVE)
if (tracker.tracking_state != Keys.STATE_STOP && tracker.tracking_state != Keys.STATE_MAPVIEW)
{
tracker.stopTracking()
tracker.state_mapview()
}
else
{
@ -276,10 +276,10 @@ class MapFragment : Fragment()
return
}
saveBestLocationState(tracker.currentBestLocation)
if (bound && tracker.trackingState != Keys.STATE_TRACKING_ACTIVE)
if (bound && (tracker.tracking_state == Keys.STATE_MAPVIEW || tracker.tracking_state == Keys.STATE_STOP))
{
tracker.removeGpsLocationListener()
tracker.removeNetworkLocationListener()
tracker.remove_gps_location_listener()
tracker.remove_network_location_listener()
tracker.trackbook.database.commit()
}
handler.removeCallbacks(redraw_runnable)
@ -350,7 +350,7 @@ class MapFragment : Fragment()
}
if (trackerService != null)
{
trackerService!!.startTracking()
trackerService!!.state_full_recording()
}
}
@ -413,19 +413,13 @@ class MapFragment : Fragment()
val newMarker: Drawable
val fillcolor: Int
val description: String
if (tracker.listeners_enabled_at == 0L)
{
fillcolor = Color.argb(64, 0, 0, 0)
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_skull_24dp)!!
description = "No location listeners are enabled"
}
else if (tracker.trackingState == Keys.STATE_TRACKING_ACTIVE && tracker.location_interval == Keys.LOCATION_INTERVAL_DEAD)
if (tracker.tracking_state == Keys.STATE_DEAD)
{
fillcolor = Color.argb(64, 0, 0, 0)
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_skull_24dp)!!
description = "GPS is struggling; disabled until movement"
}
else if (tracker.trackingState == Keys.STATE_TRACKING_ACTIVE && tracker.location_interval == Keys.LOCATION_INTERVAL_SLEEP)
else if (tracker.tracking_state == Keys.STATE_SLEEP)
{
fillcolor = Color.argb(64, 220, 61, 51)
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_sleep_24dp)!!
@ -437,7 +431,7 @@ class MapFragment : Fragment()
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_black_24dp)!!
description = "GPS tracking at full power"
}
else if (tracker.trackingState == Keys.STATE_TRACKING_ACTIVE)
else if (tracker.tracking_state == Keys.STATE_FULL_RECORDING || tracker.tracking_state == Keys.STATE_ARRIVED_AT_HOME)
{
fillcolor = Color.argb(64, 220, 61, 51)
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_red_24dp)!!
@ -591,13 +585,13 @@ class MapFragment : Fragment()
mainButton.text = requireContext().getString(R.string.button_not_ready)
mainButton.icon = null
}
else if (tracker == null || tracker.trackingState == Keys.STATE_TRACKING_STOPPED)
else if (tracker == null || tracker.tracking_state == Keys.STATE_STOP || tracker.tracking_state == Keys.STATE_MAPVIEW)
{
mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp)
mainButton.text = requireContext().getString(R.string.button_start)
mainButton.contentDescription = requireContext().getString(R.string.descr_button_start)
}
else if (tracker.trackingState == Keys.STATE_TRACKING_ACTIVE)
else
{
mainButton.setIconResource(R.drawable.ic_fiber_manual_stop_24dp)
mainButton.text = requireContext().getString(R.string.button_pause)
@ -672,11 +666,13 @@ class MapFragment : Fragment()
if (show_debug)
{
map_current_time.text = """
state: ${state_name()}
now: ${iso8601_local_noms(System.currentTimeMillis())}
location: ${iso8601_local_noms(tracker.currentBestLocation.time)}
listeners: ${iso8601_local_noms(tracker.listeners_enabled_at)}
motion: ${iso8601_local_noms(tracker.last_significant_motion)}
watchdog: ${iso8601_local_noms(tracker.last_watchdog)}
home: ${iso8601_local_noms(tracker.arrived_at_home)}
died: ${iso8601_local_noms(tracker.gave_up_at)}
power: ${tracker.device_is_charging}
wakelock: ${tracker.wakelock.isHeld}
@ -690,6 +686,25 @@ class MapFragment : Fragment()
mapView.invalidate()
}
fun state_name(): String
{
val tracker = trackerService
if (tracker == null)
{
return "null"
}
return when (tracker.tracking_state)
{
Keys.STATE_STOP -> "stop"
Keys.STATE_FULL_RECORDING -> "recording"
Keys.STATE_ARRIVED_AT_HOME -> "home"
Keys.STATE_SLEEP -> "sleep"
Keys.STATE_DEAD -> "dead"
Keys.STATE_MAPVIEW -> "mapview"
else -> tracker.tracking_state.toString()
}
}
val redraw_runnable: Runnable = object : Runnable
{
override fun run()

View file

@ -173,8 +173,8 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
if (track.trkpts.size > 0)
{
val first = track.trkpts.first()
controller.setCenter(GeoPoint(first.latitude, first.longitude))
val last = track.trkpts.last()
controller.setCenter(GeoPoint(last.latitude, last.longitude))
}
controller.setZoom(Keys.DEFAULT_ZOOM_LEVEL)

View file

@ -43,20 +43,20 @@ class TrackerService: Service()
lateinit var trackbook: Trackbook
val handler: Handler = Handler(Looper.getMainLooper())
var trackingState: Int = Keys.STATE_TRACKING_STOPPED
var tracking_state: Int = Keys.STATE_STOP
var useImperial: Boolean = false
var omitRests: Boolean = true
var max_accuracy: Float = Keys.DEFAULT_MAX_ACCURACY
var allow_sleep: Boolean = true
var device_id: String = random_device_id()
var currentBestLocation: Location = getDefaultLocation()
var lastCommit: Long = 0
var last_commit: Long = 0
var foreground_started: Long = 0
var listeners_enabled_at: Long = 0
var last_significant_motion: Long = 0
var last_watchdog: Long = 0
var gave_up_at: Long = 0
var arrived_at_home: Long = 0
var location_interval: Long = 0
val TIME_UNTIL_SLEEP: Long = 5 * Keys.ONE_MINUTE_IN_MILLISECONDS
val TIME_UNTIL_DEAD: Long = 3 * Keys.ONE_MINUTE_IN_MILLISECONDS
val WATCHDOG_INTERVAL: Long = 61 * Keys.ONE_SECOND_IN_MILLISECONDS
@ -67,7 +67,7 @@ class TrackerService: Service()
var bound: Boolean = false
private val binder = TrackerServiceBinder(this)
private lateinit var notificationManager: NotificationManager
private lateinit var notification_manager: NotificationManager
private lateinit var notification_builder: NotificationCompat.Builder
private lateinit var locationManager: LocationManager
@ -90,7 +90,7 @@ class TrackerService: Service()
lateinit var wakelock: PowerManager.WakeLock
private fun addGpsLocationListener(interval: Long): Boolean
private fun add_gps_location_listener(interval: Long): Boolean
{
gpsLocationListenerRegistered = false
gpsProviderActive = isGpsEnabled(locationManager)
@ -118,7 +118,7 @@ class TrackerService: Service()
return true
}
private fun addNetworkLocationListener(interval: Long): Boolean
private fun add_network_location_listener(interval: Long): Boolean
{
networkLocationListenerRegistered = false
networkProviderActive = isNetworkEnabled(locationManager)
@ -146,7 +146,7 @@ class TrackerService: Service()
return true
}
fun removeGpsLocationListener()
fun remove_gps_location_listener()
{
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)
{
@ -160,7 +160,7 @@ class TrackerService: Service()
}
}
fun removeNetworkLocationListener()
fun remove_network_location_listener()
{
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)
{
@ -174,67 +174,171 @@ class TrackerService: Service()
}
}
fun load_tracking_state()
{
tracking_state = PreferencesHelper.loadTrackingState()
when (tracking_state)
{
Keys.STATE_STOP -> state_stop()
Keys.STATE_MAPVIEW -> state_mapview()
Keys.STATE_FULL_RECORDING -> state_full_recording()
Keys.STATE_ARRIVED_AT_HOME -> state_arrived_at_home()
Keys.STATE_SLEEP -> state_sleep()
Keys.STATE_DEAD -> state_dead()
}
}
fun state_stop()
{
// This state is activated when the user intentionally stops the recording, or exits the
// mapfragment without starting to record.
Log.i("VOUSSOIR", "TrackerService.state_stop")
tracking_state = Keys.STATE_STOP
PreferencesHelper.saveTrackingState(tracking_state)
reset_location_listeners(Keys.LOCATION_INTERVAL_STOP)
trackbook.database.commit()
recent_displacement_locations.clear()
arrived_at_home = 0
gave_up_at = 0
if (foreground_started > 0)
{
stopForeground(STOP_FOREGROUND_DETACH)
foreground_started = 0
}
stop_wakelock()
displayNotification()
}
fun state_full_recording()
{
// This state is the only one that will record points into the database, and tracks location
// at full power. A wakelock is used to resist Android's doze. This state should be active
// while out and about.
Log.i("VOUSSOIR", "TrackerService.state_full_power")
tracking_state = Keys.STATE_FULL_RECORDING
PreferencesHelper.saveTrackingState(tracking_state)
reset_location_listeners(Keys.LOCATION_INTERVAL_FULL_POWER)
arrived_at_home = 0
gave_up_at = 0
if (foreground_started == 0L)
{
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
foreground_started = System.currentTimeMillis()
}
if (gpsLocationListenerRegistered || networkLocationListenerRegistered)
{
start_wakelock()
}
else
{
state_dead()
}
displayNotification()
}
fun state_arrived_at_home()
{
// This state is activated when the user enters the radius of a homepoint. The GPS will
// remain at full power for a few minutes before we transition to sleep.
Log.i("VOUSSOIR", "TrackerService.state_arrived_at_home")
tracking_state = Keys.STATE_ARRIVED_AT_HOME
PreferencesHelper.saveTrackingState(tracking_state)
reset_location_listeners(Keys.LOCATION_INTERVAL_FULL_POWER)
trackbook.database.commit()
arrived_at_home = System.currentTimeMillis()
gave_up_at = 0
stop_wakelock()
displayNotification()
}
fun state_sleep()
{
// This state is activated when the user stays at a homepoint for several minutes. It will
// be woken up again by the acceleromters or by plugging / unplugging power.
Log.i("VOUSSOIR", "TrackerService.state_sleep")
tracking_state = Keys.STATE_SLEEP
PreferencesHelper.saveTrackingState(tracking_state)
reset_location_listeners(Keys.LOCATION_INTERVAL_SLEEP)
arrived_at_home = arrived_at_home
gave_up_at = 0
stop_wakelock()
displayNotification()
}
fun state_dead()
{
// This state is activated when the device is struggling to receive a GPS fix due to being
// indoors / underground. It will be woken up again by the accelerometers or by plugging /
// unplugging power.
Log.i("VOUSSOIR", "TrackerService.state_dead")
tracking_state = Keys.STATE_DEAD
PreferencesHelper.saveTrackingState(tracking_state)
reset_location_listeners(Keys.LOCATION_INTERVAL_STOP)
trackbook.database.commit()
recent_displacement_locations.clear()
arrived_at_home = 0
gave_up_at = System.currentTimeMillis()
stop_wakelock()
displayNotification()
}
fun state_mapview()
{
// This state should be activated when the user has the app open to the mapfragment, but is
// not recording. If the user closes the app while in this state, we can go to stop.
Log.i("VOUSSOIR", "TrackerService.state_mapview")
tracking_state = Keys.STATE_MAPVIEW
PreferencesHelper.saveTrackingState(tracking_state)
reset_location_listeners(Keys.LOCATION_INTERVAL_FULL_POWER)
arrived_at_home = 0
gave_up_at = 0
stop_wakelock()
displayNotification()
if (!gpsLocationListenerRegistered && !networkLocationListenerRegistered)
{
state_dead()
}
}
fun start_wakelock()
{
if (!wakelock.isHeld)
{
wakelock.acquire()
}
}
fun stop_wakelock()
{
if (wakelock.isHeld)
{
wakelock.release()
}
}
fun reset_location_listeners(interval: Long)
{
Log.i("VOUSSOIR", "TrackerService.reset_location_listeners")
location_interval = interval
if (use_gps_location && interval != Keys.LOCATION_INTERVAL_DEAD && interval != Keys.LOCATION_INTERVAL_STOP)
if (use_gps_location && interval != Keys.LOCATION_INTERVAL_STOP)
{
addGpsLocationListener(interval)
add_gps_location_listener(interval)
}
else if (gpsLocationListenerRegistered)
{
removeGpsLocationListener()
remove_gps_location_listener()
}
if (use_network_location && interval != Keys.LOCATION_INTERVAL_DEAD && interval != Keys.LOCATION_INTERVAL_STOP)
if (use_network_location && interval != Keys.LOCATION_INTERVAL_STOP)
{
addNetworkLocationListener(interval)
add_network_location_listener(interval)
}
else if (networkLocationListenerRegistered)
{
removeNetworkLocationListener()
}
if (interval != Keys.LOCATION_INTERVAL_DEAD)
{
gave_up_at = 0
remove_network_location_listener()
}
if (gpsLocationListenerRegistered || networkLocationListenerRegistered)
{
listeners_enabled_at = System.currentTimeMillis()
if (interval != Keys.LOCATION_INTERVAL_SLEEP)
{
arrived_at_home = 0
}
}
else if (interval == Keys.LOCATION_INTERVAL_STOP)
{
listeners_enabled_at = 0
}
else
{
listeners_enabled_at = 0
location_interval = Keys.LOCATION_INTERVAL_DEAD
}
val should_wakelock = (
(gpsLocationListenerRegistered || networkLocationListenerRegistered) &&
trackingState == Keys.STATE_TRACKING_ACTIVE &&
interval == Keys.LOCATION_INTERVAL_FULL_POWER
)
if (should_wakelock)
{
if (! wakelock.isHeld)
{
wakelock.acquire()
}
}
else if (wakelock.isHeld)
{
wakelock.release()
}
displayNotification()
}
@ -253,10 +357,11 @@ class TrackerService: Service()
currentBestLocation = location
if (trackingState != Keys.STATE_TRACKING_ACTIVE)
if (tracking_state == Keys.STATE_STOP || tracking_state == Keys.STATE_MAPVIEW)
{
return
}
if(! trackbook.database.ready)
{
Log.i("VOUSSOIR", "Omitting due to database not ready.")
@ -283,34 +388,43 @@ class TrackerService: Service()
continue
}
Log.i("VOUSSOIR", "Omitting due to homepoint ${index} ${homepoint.name}.")
// Move this homepoint to the front of the list so that on subsequent location
// updates it hits on the first loop. I'm sure this is a trivial amount of
// savings but oh well.
if (index > 0)
{
trackbook.homepoints.remove(homepoint)
trackbook.homepoints.addFirst(homepoint)
}
if (arrived_at_home == 0L)
if (tracking_state != Keys.STATE_ARRIVED_AT_HOME && tracking_state != Keys.STATE_SLEEP)
{
Log.i("VOUSSOIR", "Arrived at home.")
arrived_at_home = System.currentTimeMillis()
trackbook.database.commit()
state_arrived_at_home()
}
else if (
allow_sleep &&
has_motion_sensor &&
location_interval != Keys.LOCATION_INTERVAL_SLEEP &&
tracking_state == Keys.STATE_ARRIVED_AT_HOME &&
(System.currentTimeMillis() - arrived_at_home) > TIME_UNTIL_SLEEP &&
(System.currentTimeMillis() - last_significant_motion) > TIME_UNTIL_SLEEP
)
{
Log.i("VOUSSOIR", "Staying at home, sleeping.")
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_SLEEP)
state_sleep()
}
return
}
if (arrived_at_home > 0)
// All of the homepoint checks failed so we have left home and it's time to wake up.
// In practice we expect that the accelerometers would have triggered this change
// already, but this acts as a backup in case you somehow leave home without too
// much movement (maybe you sat in the car for several minutes before finally
// driving away or something).
if (tracking_state == Keys.STATE_ARRIVED_AT_HOME || tracking_state == Keys.STATE_SLEEP)
{
Log.i("VOUSSOIR", "Leaving home.")
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
state_full_recording()
}
if (! isRecentEnough(location))
@ -351,10 +465,10 @@ class TrackerService: Service()
recent_displacement_locations.removeFirst()
}
if (location.time - lastCommit > Keys.COMMIT_INTERVAL)
if ((location.time - last_commit) > Keys.COMMIT_INTERVAL)
{
trackbook.database.commit()
lastCommit = location.time
last_commit = location.time
}
}
override fun onProviderEnabled(provider: String)
@ -385,43 +499,34 @@ class TrackerService: Service()
}
val timestamp = iso8601_local_noms(currentBestLocation.time)
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
if (tracking_state == Keys.STATE_FULL_RECORDING)
{
notification_builder.setContentTitle(this.getString(R.string.notification_title_trackbook_running))
if (location_interval == Keys.LOCATION_INTERVAL_FULL_POWER && arrived_at_home > 0)
{
notification_builder.setContentTitle("${timestamp} (home)")
notification_builder.setSmallIcon(R.drawable.ic_satellite_24dp)
}
else if (location_interval == Keys.LOCATION_INTERVAL_FULL_POWER)
{
notification_builder.setContentTitle("${timestamp} (recording)")
notification_builder.setSmallIcon(R.drawable.ic_satellite_24dp)
}
else if (location_interval == Keys.LOCATION_INTERVAL_SLEEP)
{
notification_builder.setContentTitle("${timestamp} (sleeping)")
notification_builder.setSmallIcon(R.drawable.ic_sleep_24dp)
}
else if (location_interval == Keys.LOCATION_INTERVAL_DEAD)
{
notification_builder.setContentTitle("${timestamp} (dead)")
notification_builder.setSmallIcon(R.drawable.ic_skull_24dp)
}
else
{
notification_builder.setContentText(timestamp)
notification_builder.setSmallIcon(R.drawable.ic_fiber_manual_record_inactive_24dp)
}
notification_builder.setContentTitle("${timestamp} (recording)")
notification_builder.setSmallIcon(R.drawable.ic_satellite_24dp)
}
else
else if (tracking_state == Keys.STATE_ARRIVED_AT_HOME)
{
notification_builder.setContentTitle("${timestamp} (home)")
notification_builder.setSmallIcon(R.drawable.ic_homepoint_24dp)
}
else if (tracking_state == Keys.STATE_SLEEP)
{
notification_builder.setContentTitle("${timestamp} (sleeping)")
notification_builder.setSmallIcon(R.drawable.ic_sleep_24dp)
}
else if (tracking_state == Keys.STATE_DEAD)
{
notification_builder.setContentTitle("${timestamp} (dead)")
notification_builder.setSmallIcon(R.drawable.ic_skull_24dp)
}
else if (tracking_state == Keys.STATE_STOP || tracking_state == Keys.STATE_MAPVIEW)
{
notification_builder.setContentTitle("${timestamp} (stopped)")
notification_builder.setSmallIcon(R.drawable.ic_fiber_manual_stop_24dp)
}
val notification = notification_builder.build()
notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification)
notification_manager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification)
return notification
}
@ -430,7 +535,7 @@ class TrackerService: Service()
/* Checks if notification channel exists */
@RequiresApi(Build.VERSION_CODES.O)
private fun nowPlayingChannelExists() = notificationManager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null
private fun nowPlayingChannelExists() = notification_manager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null
/* Create a notification channel */
@RequiresApi(Build.VERSION_CODES.O)
@ -441,18 +546,21 @@ class TrackerService: Service()
this.getString(R.string.notification_channel_recording_name),
NotificationManager.IMPORTANCE_LOW
).apply { description = this@TrackerService.getString(R.string.notification_channel_recording_description) }
notificationManager.createNotificationChannel(notificationChannel)
notification_manager.createNotificationChannel(notificationChannel)
}
/* Overrides onBind from Service */
override fun onBind(p0: Intent?): IBinder
{
Log.i("VOUSSOIR", "TrackerService.onBind")
if (listeners_enabled_at == 0L && location_interval != Keys.LOCATION_INTERVAL_DEAD)
if (tracking_state == Keys.STATE_STOP)
{
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
state_mapview()
}
else
{
displayNotification()
}
displayNotification()
bound = true
return binder
}
@ -461,11 +569,14 @@ class TrackerService: Service()
override fun onRebind(intent: Intent?)
{
Log.i("VOUSSOIR", "TrackerService.onRebind")
if (listeners_enabled_at == 0L && location_interval != Keys.LOCATION_INTERVAL_DEAD)
if (tracking_state == Keys.STATE_STOP)
{
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
state_mapview()
}
else
{
displayNotification()
}
displayNotification()
bound = true
}
@ -475,10 +586,11 @@ class TrackerService: Service()
super.onUnbind(intent)
Log.i("VOUSSOIR", "TrackerService.onUnbind")
bound = false
// stop receiving location updates - if not tracking
if (trackingState != Keys.STATE_TRACKING_ACTIVE)
// the user was only perusing the map and did not start recording, so we'll just stop.
if (tracking_state == Keys.STATE_MAPVIEW)
{
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_STOP)
state_stop()
}
// ensures onRebind is called
return true
@ -498,9 +610,10 @@ class TrackerService: Service()
device_id = PreferencesHelper.load_device_id()
useImperial = PreferencesHelper.loadUseImperialUnits()
omitRests = PreferencesHelper.loadOmitRests()
max_accuracy = PreferencesHelper.load_max_accuracy()
allow_sleep = PreferencesHelper.loadAllowSleep()
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notification_manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notification_builder = NotificationCompat.Builder(this, Keys.NOTIFICATION_CHANNEL_RECORDING)
val showActionPendingIntent: PendingIntent? = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(Intent(this@TrackerService, MainActivity::class.java))
@ -512,7 +625,6 @@ class TrackerService: Service()
networkProviderActive = isNetworkEnabled(locationManager)
gpsLocationListener = createLocationListener()
networkLocationListener = createLocationListener()
trackingState = PreferencesHelper.loadTrackingState()
currentBestLocation = getLastKnownLocation(this)
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
@ -526,10 +638,10 @@ class TrackerService: Service()
override fun onTrigger(event: TriggerEvent?) {
Log.i("VOUSSOIR", "Significant motion")
last_significant_motion = System.currentTimeMillis()
if (trackingState == Keys.STATE_TRACKING_ACTIVE && location_interval != Keys.LOCATION_INTERVAL_FULL_POWER)
if (tracking_state == Keys.STATE_SLEEP || tracking_state == Keys.STATE_DEAD)
{
vibrator.vibrate(100)
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
state_full_recording()
}
sensor_manager.requestTriggerSensor(this, significant_motion_sensor)
}
@ -546,11 +658,10 @@ class TrackerService: Service()
override fun onSensorChanged(event: SensorEvent?) {
Log.i("VOUSSOIR", "Step counter changed")
last_significant_motion = System.currentTimeMillis()
if (trackingState == Keys.STATE_TRACKING_ACTIVE && location_interval != Keys.LOCATION_INTERVAL_FULL_POWER)
if (tracking_state == Keys.STATE_SLEEP || tracking_state == Keys.STATE_DEAD)
{
// beeper.startTone(ToneGenerator.TONE_PROP_ACK, 150)
vibrator.vibrate(100)
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
state_full_recording()
}
}
@ -574,9 +685,9 @@ class TrackerService: Service()
{
device_is_charging = false
}
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
if (tracking_state == Keys.STATE_SLEEP || tracking_state == Keys.STATE_DEAD)
{
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
state_full_recording()
}
}
}
@ -588,6 +699,7 @@ class TrackerService: Service()
val powermanager = getSystemService(Context.POWER_SERVICE) as PowerManager
wakelock = powermanager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "trkpt::wakelock")
load_tracking_state()
handler.post(background_watchdog)
}
@ -599,19 +711,19 @@ class TrackerService: Service()
// SERVICE RESTART (via START_STICKY)
if (intent == null)
{
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
if (tracking_state != Keys.STATE_STOP && tracking_state != Keys.STATE_MAPVIEW)
{
Log.w("VOUSSOIR", "Trackbook has been killed by the operating system. Trying to resume recording.")
startTracking()
state_full_recording()
}
}
else if (intent.action == Keys.ACTION_STOP)
{
stopTracking()
state_stop()
}
else if (intent.action == Keys.ACTION_START)
{
startTracking()
state_full_recording()
}
// START_STICKY is used for services that are explicitly started and stopped as needed
@ -623,51 +735,24 @@ class TrackerService: Service()
{
Log.i("VOUSSOIR", "TrackerService.onDestroy.")
super.onDestroy()
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
{
stopTracking()
}
stopForeground(STOP_FOREGROUND_REMOVE)
state_stop()
PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener)
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_DEAD)
handler.removeCallbacks(background_watchdog)
unregisterReceiver(charging_broadcast_receiver)
}
fun startTracking()
{
Log.i("VOUSSOIR", "TrackerService.startTracking")
trackingState = Keys.STATE_TRACKING_ACTIVE
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
PreferencesHelper.saveTrackingState(trackingState)
recent_displacement_locations.clear()
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
}
fun stopTracking()
{
Log.i("VOUSSOIR", "TrackerService.stopTracking")
trackbook.database.commit()
trackingState = Keys.STATE_TRACKING_STOPPED
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
PreferencesHelper.saveTrackingState(trackingState)
recent_displacement_locations.clear()
displayNotification()
stopForeground(STOP_FOREGROUND_DETACH)
}
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key)
{
Keys.PREF_LOCATION_GPS ->
{
use_gps_location = PreferencesHelper.load_location_gps()
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
state_full_recording()
}
Keys.PREF_LOCATION_NETWORK ->
{
use_network_location = PreferencesHelper.load_location_network()
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
state_full_recording()
}
Keys.PREF_USE_IMPERIAL_UNITS ->
{
@ -684,9 +769,9 @@ class TrackerService: Service()
Keys.PREF_ALLOW_SLEEP ->
{
allow_sleep = PreferencesHelper.loadAllowSleep()
if (! allow_sleep && trackingState == Keys.STATE_TRACKING_ACTIVE && location_interval != Keys.LOCATION_INTERVAL_FULL_POWER)
if (! allow_sleep && (tracking_state == Keys.STATE_SLEEP || tracking_state == Keys.STATE_DEAD))
{
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_FULL_POWER)
state_full_recording()
}
}
Keys.PREF_DEVICE_ID ->
@ -723,21 +808,19 @@ class TrackerService: Service()
has_motion_sensor &&
!device_is_charging &&
// We only go to dead during active tracking because if you are looking at the
// device in non-tracking mode you are probably waiting for your signal to recover.
trackingState == Keys.STATE_TRACKING_ACTIVE &&
// device in mapview state you are probably waiting for your signal to recover.
// We only go to dead from full power because in the sleep state, the wakelock is
// turned off and the device may go into doze. During doze, the device stops
// updating the location listeners anyway, so there is no benefit in going to dead.
// When the user interacts with the device and it leaves doze, it's better to come
// from sleep state than dead state.
location_interval == Keys.LOCATION_INTERVAL_FULL_POWER &&
tracking_state == Keys.STATE_FULL_RECORDING &&
(now - listeners_enabled_at) > TIME_UNTIL_DEAD &&
(now - currentBestLocation.time) > TIME_UNTIL_DEAD &&
(now - last_significant_motion) > TIME_UNTIL_DEAD
)
{
reset_location_listeners(interval=Keys.LOCATION_INTERVAL_DEAD)
gave_up_at = now
state_dead()
}
}
}

View file

@ -1,142 +0,0 @@
/*
* TrackingToggleTileService.kt
* Implements the TrackingToggleTileService service
* A TrackingToggleTileService toggles the recording state from a quick settings tile
*
* 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
*/
/*
* Modified by voussoir for trkpt, forked from Trackbook.
*/
package net.voussoir.trkpt
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import net.voussoir.trkpt.helpers.PreferencesHelper
/*
* TrackingToggleTileService class
*/
class TrackingToggleTileService: TileService()
{
/* Main class variables */
private var bound: Boolean = false
private var trackingState: Int = Keys.STATE_TRACKING_STOPPED
private lateinit var trackerService: TrackerService
/* Overrides onTileAdded from TileService */
override fun onTileAdded() {
super.onTileAdded()
// get saved tracking state
trackingState = PreferencesHelper.loadTrackingState()
// set up tile
updateTile()
}
/* Overrides onTileRemoved from TileService */
override fun onTileRemoved() {
super.onTileRemoved()
}
/* Overrides onStartListening from TileService (tile becomes visible) */
override fun onStartListening() {
super.onStartListening()
// get saved tracking state
trackingState = PreferencesHelper.loadTrackingState()
// set up tile
updateTile()
// register listener for changes in shared preferences
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
}
/* Overrides onClick from TileService */
override fun onClick() {
super.onClick()
when (trackingState) {
Keys.STATE_TRACKING_ACTIVE -> stopTracking()
else -> startTracking()
}
}
/* Overrides onStopListening from TileService (tile no longer visible) */
override fun onStopListening() {
super.onStopListening()
// unregister listener for changes in shared preferences
PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener)
}
/* Overrides onDestroy from Service */
override fun onDestroy() {
super.onDestroy()
}
/* Update quick settings tile */
private fun updateTile() {
val tile: Tile = qsTile
tile.icon = Icon.createWithResource(this, R.drawable.ic_notification_icon_small_24dp)
when (trackingState) {
Keys.STATE_TRACKING_ACTIVE -> {
tile.label = getString(R.string.quick_settings_tile_title_pause)
tile.contentDescription = getString(R.string.descr_quick_settings_tile_title_pause)
tile.state = Tile.STATE_ACTIVE
}
else -> {
tile.label = getString(R.string.quick_settings_tile_title_start)
tile.contentDescription = getString(R.string.descr_quick_settings_tile_title_start)
tile.state = Tile.STATE_INACTIVE
}
}
tile.updateTile()
}
/* Start tracking */
private fun startTracking() {
val intent = Intent(application, TrackerService::class.java)
intent.action = Keys.ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// ... start service in foreground to prevent it being killed on Oreo
application.startForegroundService(intent)
} else {
application.startService(intent)
}
}
/* Stop tracking */
private fun stopTracking() {
val intent = Intent(application, TrackerService::class.java)
intent.action = Keys.ACTION_STOP
application.startService(intent)
}
/*
* Defines the listener for changes in shared preferences
*/
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) {
Keys.PREF_TRACKING_STATE -> {
trackingState = PreferencesHelper.loadTrackingState()
updateTile()
}
}
}
/*
* End of declaration
*/
}

View file

@ -75,7 +75,7 @@ object PreferencesHelper
}
fun loadTrackingState(): Int {
return sharedPreferences.getInt(Keys.PREF_TRACKING_STATE, Keys.STATE_TRACKING_STOPPED)
return sharedPreferences.getInt(Keys.PREF_TRACKING_STATE, Keys.STATE_STOP)
}
fun saveTrackingState(trackingState: Int) {

View file

@ -30,6 +30,7 @@ import net.voussoir.trkpt.Keys
import net.voussoir.trkpt.R
import net.voussoir.trkpt.Database
import net.voussoir.trkpt.Track
import net.voussoir.trkpt.helpers.PreferencesHelper
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@ -53,9 +54,10 @@ class TracklistAdapter(val fragment: Fragment, val database: Database) : Recycle
{
return
}
val max_accuracy = PreferencesHelper.load_max_accuracy()
val cursor: Cursor = database.connection.rawQuery(
"SELECT distinct(date(time/1000, 'unixepoch', 'localtime')) as thedate, device_id FROM trkpt ORDER BY thedate DESC",
arrayOf()
"SELECT distinct(date(time/1000, 'unixepoch', 'localtime')) as thedate, device_id FROM trkpt WHERE accuracy <= ? ORDER BY thedate DESC",
arrayOf(max_accuracy.toString())
)
try
{

View file

@ -91,7 +91,7 @@
<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>
<string name="pref_general_title">General</string>
<string name="pref_general_title">Settings</string>
<string name="pref_maintenance_title">Maintenance</string>
<string name="pref_location_gps_title">Use GPS location</string>
<string name="pref_location_gps_summary_on">GPS location enabled.</string>