trkpt/app/src/main/java/org/y20k/trackbook/TrackerService.kt

481 lines
19 KiB
Kotlin
Raw Normal View History

2020-01-02 17:00:37 +00:00
/*
* TrackerService.kt
* Implements the app's movement tracker service
* The TrackerService keeps track of the current location
*
* This file is part of
* TRACKBOOK - Movement Recorder for Android
*
* Copyright (c) 2016-20 - Y20K.org
* Licensed under the MIT-License
* http://opensource.org/licenses/MIT
*
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
* https://github.com/osmdroid/osmdroid
*/
package org.y20k.trackbook
import android.Manifest
import android.app.Notification
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Binder
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import kotlinx.coroutines.*
import org.y20k.trackbook.core.Track
import org.y20k.trackbook.helpers.*
import java.util.*
import kotlin.coroutines.CoroutineContext
/*
* TrackerService class
*/
class TrackerService(): Service(), CoroutineScope, SensorEventListener {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackerService::class.java)
/* Main class variables */
2020-01-28 16:39:45 +00:00
var trackingState: Int = Keys.STATE_TRACKING_NOT
2020-01-02 17:00:37 +00:00
var gpsProviderActive: Boolean = false
var networkProviderActive: Boolean = false
var useImperial: Boolean = false
var gpsOnly: Boolean = false
var locationAccuracyThreshold: Int = Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY
var currentBestLocation: Location = LocationHelper.getDefaultLocation()
var stepCountOffset: Float = 0f
var resumed: Boolean = false
2020-01-02 17:00:37 +00:00
var track: Track = Track()
var gpsLocationListenerRegistered: Boolean = false
var networkLocationListenerRegistered: Boolean = false
2020-02-06 14:55:15 +00:00
var bound: Boolean = false
2020-01-02 17:00:37 +00:00
private val binder = LocalBinder()
private val handler: Handler = Handler()
private lateinit var locationManager: LocationManager
private lateinit var sensorManager: SensorManager
private lateinit var notificationManager: NotificationManager
private lateinit var notificationHelper: NotificationHelper
private lateinit var gpsLocationListener: LocationListener
private lateinit var networkLocationListener: LocationListener
private lateinit var backgroundJob: Job
/* Overrides coroutineContext variable */
override val coroutineContext: CoroutineContext get() = backgroundJob + Dispatchers.Main
/* Overrides onCreate from Service */
override fun onCreate() {
super.onCreate()
2020-01-09 12:23:26 +00:00
gpsOnly = PreferencesHelper.loadGpsOnly(this)
useImperial = PreferencesHelper.loadUseImperialUnits(this)
locationAccuracyThreshold = PreferencesHelper.loadAccuracyThreshold(this)
2020-01-02 17:00:37 +00:00
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
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)
gpsLocationListener = createLocationListener()
networkLocationListener = createLocationListener()
trackingState = PreferencesHelper.loadTrackingState(this)
currentBestLocation = LocationHelper.getLastKnownLocation(this)
track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this))
backgroundJob = Job()
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
}
/* Overrides onStartCommand from Service */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// SERVICE RESTART (via START_STICKY)
if (intent == null) {
if (trackingState == Keys.STATE_TRACKING_ACTIVE) {
LogHelper.w(TAG, "Trackbook has been killed by the operating system. Trying to resume recording.")
resumeTracking()
}
// ACTION STOP
} else if (Keys.ACTION_STOP == intent.action) {
stopTracking()
// ACTION START
} else if (Keys.ACTION_START == intent.action) {
startTracking()
// ACTION RESUME
} else if (Keys.ACTION_RESUME == intent.action) {
resumeTracking()
}
// START_STICKY is used for services that are explicitly started and stopped as needed
return START_STICKY
2020-01-02 17:00:37 +00:00
}
/* Overrides onBind from Service */
override fun onBind(p0: Intent?): IBinder? {
2020-02-06 14:55:15 +00:00
bound = true
// start receiving location updates
2020-01-09 12:23:26 +00:00
addGpsLocationListener()
addNetworkLocationListener()
2020-02-06 14:55:15 +00:00
// return reference to this service
2020-01-02 17:00:37 +00:00
return binder
}
2020-02-06 14:55:15 +00:00
/* Overrides onRebind from Service */
override fun onRebind(intent: Intent?) {
bound = true
// start receiving location updates
addGpsLocationListener()
addNetworkLocationListener()
}
/* Overrides onUnbind from Service */
override fun onUnbind(intent: Intent?): Boolean {
bound = false
// stop receiving location updates - if not tracking
if (trackingState != Keys.STATE_TRACKING_ACTIVE) {
removeGpsLocationListener()
removeNetworkLocationListener()
}
// ensures onRebind is called
return true
}
2020-03-13 12:34:37 +00:00
2020-01-02 17:00:37 +00:00
/* Overrides onDestroy from Service */
override fun onDestroy() {
super.onDestroy()
LogHelper.i(TAG, "onDestroy called.")
2020-02-06 14:55:15 +00:00
// stop tracking
2020-01-02 17:00:37 +00:00
if (trackingState == Keys.STATE_TRACKING_ACTIVE) stopTracking()
2020-02-06 14:55:15 +00:00
// remove notification
2020-01-02 17:00:37 +00:00
stopForeground(true)
2020-02-06 14:55:15 +00:00
// stop listening for changes in shared preferences
2020-01-02 17:00:37 +00:00
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
2020-02-06 14:55:15 +00:00
// stop receiving location updates
2020-01-09 12:23:26 +00:00
removeGpsLocationListener()
removeNetworkLocationListener()
2020-02-06 14:55:15 +00:00
// cancel background job
2020-01-02 17:00:37 +00:00
backgroundJob.cancel()
}
/* Overrides onAccuracyChanged from SensorEventListener */
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
LogHelper.v(TAG, "Accuracy changed: $accuracy")
}
/* Overrides onSensorChanged from SensorEventListener */
override fun onSensorChanged(sensorEvent: SensorEvent?) {
var steps: Float = 0f
if (sensorEvent != null) {
if (stepCountOffset == 0f) {
// store steps previously recorded by the system
stepCountOffset = (sensorEvent.values[0] - 1) - track.stepCount // subtract any steps recorded during this session in case the app was killed
}
// calculate step count - subtract steps previously recorded
steps = sensorEvent.values[0] - stepCountOffset
}
// update step count in track
track.stepCount = steps
}
/* Resume tracking after stop/pause */
fun resumeTracking() {
// load temp track - returns an empty track if not available
track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this))
// try to mark last waypoint as stopover
if (track.wayPoints.size > 0) {
val lastWayPointIndex = track.wayPoints.size - 1
track.wayPoints.get(lastWayPointIndex).isStopOver = true
}
// set resumed flag
resumed = true
// calculate length of recording break
track.recordingPaused = TrackHelper.calculateRecordingPaused(track.recordingStop)
2020-01-02 17:00:37 +00:00
// start tracking
startTracking(newTrack = false)
}
/* Start tracking location */
fun startTracking(newTrack: Boolean = true) {
2020-02-06 14:55:15 +00:00
// start receiving location updates
addGpsLocationListener()
addNetworkLocationListener()
// set up new track
2020-01-02 17:00:37 +00:00
if (newTrack) {
track = Track()
2020-01-02 17:00:37 +00:00
track.recordingStart = GregorianCalendar.getInstance().time
track.recordingStop = track.recordingStart
track.name = DateTimeHelper.convertToReadableDate(track.recordingStart)
stepCountOffset = 0f
}
2020-02-06 14:55:15 +00:00
// set state
2020-01-02 17:00:37 +00:00
trackingState = Keys.STATE_TRACKING_ACTIVE
PreferencesHelper.saveTrackingState(this, trackingState)
2020-02-06 14:55:15 +00:00
// start recording steps and location fixes
2020-01-02 17:00:37 +00:00
startStepCounter()
handler.postDelayed(periodicTrackUpdate, 0)
2020-02-06 14:55:15 +00:00
// show notification
2020-01-02 17:00:37 +00:00
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
}
/* Stop tracking location */
fun stopTracking() {
2020-02-06 14:55:15 +00:00
// stop receiving location updates - if app is not bound (= visible)
if (!bound) {
removeGpsLocationListener()
removeGpsLocationListener()
}
// save state
2020-01-02 17:00:37 +00:00
track.recordingStop = GregorianCalendar.getInstance().time
trackingState = Keys.STATE_TRACKING_STOPPED
PreferencesHelper.saveTrackingState(this, trackingState)
2020-02-06 14:55:15 +00:00
// stop recording steps and location fixes
2020-01-02 17:00:37 +00:00
sensorManager.unregisterListener(this)
handler.removeCallbacks(periodicTrackUpdate)
2020-02-06 14:55:15 +00:00
// update notification
2020-01-02 17:00:37 +00:00
displayNotification()
stopForeground(false)
}
/* Clear track recording */
fun clearTrack() {
track = Track()
FileHelper.deleteTempFile(this)
2020-01-28 16:39:45 +00:00
trackingState = Keys.STATE_TRACKING_NOT
2020-01-02 17:00:37 +00:00
PreferencesHelper.saveTrackingState(this, trackingState)
stopForeground(true)
}
/* Saves track recording to storage */
fun saveTrack() {
// save track using "deferred await"
launch {
// step 1: create and store filenames for json and gpx files
track.trackUriString = FileHelper.getTrackFileUri(this@TrackerService, track).toString()
track.gpxUriString = FileHelper.getGpxFileUri(this@TrackerService, track).toString()
// step 2: save track
FileHelper.saveTrackSuspended(track, saveGpxToo = true)
// step 3: save tracklist
FileHelper.addTrackAndSaveTracklistSuspended(this@TrackerService, track)
// step 3: clear track
clearTrack()
}
}
/* Creates location listener */
private fun createLocationListener(): LocationListener {
return object : LocationListener {
override fun onLocationChanged(location: Location) {
// update currentBestLocation if a better location is available
if (LocationHelper.isBetterLocation(location, currentBestLocation)) {
currentBestLocation = location
}
}
override fun onProviderEnabled(provider: String) {
LogHelper.v(TAG, "onProviderEnabled $provider")
when (provider) {
LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
LocationManager.NETWORK_PROVIDER -> networkProviderActive = LocationHelper.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)
}
}
override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) {
// deprecated method
}
}
}
2020-01-09 12:23:26 +00:00
/* Adds a GPS location listener to location manager */
2020-02-06 14:55:15 +00:00
private fun addGpsLocationListener() {
// check if already registered
if (!gpsLocationListenerRegistered) {
// check if Network provider is available
if (gpsProviderActive) {
// check for location permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
// adds GPS location listener
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f,gpsLocationListener)
gpsLocationListenerRegistered = true
LogHelper.v(TAG, "Added GPS location listener.")
} else {
LogHelper.w(TAG, "Unable to add GPS location listener. Location permission is not granted.")
}
2020-01-09 12:23:26 +00:00
} else {
LogHelper.w(TAG, "Unable to add GPS location listener.")
2020-01-02 17:00:37 +00:00
}
2020-01-09 12:23:26 +00:00
} else {
2020-02-06 14:55:15 +00:00
LogHelper.v(TAG, "Skipping registration. GPS location listener has already been added.")
2020-01-09 12:23:26 +00:00
}
}
/* Adds a Network location listener to location manager */
2020-02-06 14:55:15 +00:00
private fun addNetworkLocationListener() {
// check if already registered
if (!networkLocationListenerRegistered) {
// check if Network provider is available
if (networkProviderActive && !gpsOnly) {
// check for location permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
// adds Network location listener
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0f, networkLocationListener)
networkLocationListenerRegistered = true
LogHelper.v(TAG, "Added Network location listener.")
} else {
LogHelper.w(TAG, "Unable to add Network location listener. Location permission is not granted.")
}
2020-01-09 12:23:26 +00:00
} else {
LogHelper.w(TAG, "Unable to add Network location listener.")
2020-01-02 17:00:37 +00:00
}
} else {
2020-02-06 14:55:15 +00:00
LogHelper.v(TAG, "Skipping registration. Network location listener has already been added.")
2020-01-02 17:00:37 +00:00
}
}
2020-01-09 12:23:26 +00:00
/* Adds location listeners to location manager */
fun removeGpsLocationListener() {
2020-01-09 12:23:26 +00:00
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationManager.removeUpdates(gpsLocationListener)
gpsLocationListenerRegistered = false
2020-01-09 12:23:26 +00:00
LogHelper.v(TAG, "Removed GPS location listener.")
} else {
LogHelper.w(TAG, "Unable to remove GPS location listener. Location permission is needed.")
}
}
/* Adds location listeners to location manager */
fun removeNetworkLocationListener() {
2020-01-09 12:23:26 +00:00
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationManager.removeUpdates(gpsLocationListener)
networkLocationListenerRegistered = false
2020-01-09 12:23:26 +00:00
LogHelper.v(TAG, "Removed Network location listener.")
} else {
LogHelper.w(TAG, "Unable to remove Network location listener. Location permission is needed.")
}
2020-01-02 17:00:37 +00:00
}
/* Registers a step counter listener */
private fun startStepCounter() {
val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI)
if (!stepCounterAvailable) {
LogHelper.w(TAG, "Pedometer sensor not available.")
track.stepCount = -1f
}
}
/* Displays / updates notification */
private fun displayNotification(): Notification {
val notification: Notification = notificationHelper.createNotification(trackingState, track.length, track.duration, useImperial)
notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification)
return notification
}
/*
* Defines the listener for changes in shared preferences
*/
2020-01-09 12:23:26 +00:00
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
2020-01-02 17:00:37 +00:00
when (key) {
2020-02-06 14:55:15 +00:00
// preference "Restrict to GPS"
2020-01-09 12:23:26 +00:00
Keys.PREF_GPS_ONLY -> {
gpsOnly = PreferencesHelper.loadGpsOnly(this@TrackerService)
when (gpsOnly) {
true -> removeNetworkLocationListener()
false -> addNetworkLocationListener()
}
}
2020-02-06 14:55:15 +00:00
// preference "Use Imperial Measurements"
2020-01-09 12:23:26 +00:00
Keys.PREF_USE_IMPERIAL_UNITS -> {
useImperial = PreferencesHelper.loadUseImperialUnits(this@TrackerService)
}
2020-02-06 14:55:15 +00:00
// preference "Accuracy Threshold"
2020-01-09 12:23:26 +00:00
Keys.PREF_LOCATION_ACCURACY_THRESHOLD -> {
locationAccuracyThreshold = PreferencesHelper.loadAccuracyThreshold(this@TrackerService)
}
2020-01-02 17:00:37 +00:00
}
}
/*
* End of declaration
*/
/*
* Inner class: Local Binder that returns this service
*/
inner class LocalBinder : Binder() {
val service: TrackerService = this@TrackerService
}
/*
* End of inner class
*/
/*
* Runnable: Periodically track updates (if recording active)
*/
private val periodicTrackUpdate: Runnable = object : Runnable {
override fun run() {
// add waypoint to track - step count is continuously updated in onSensorChanged
val result: Pair<Track, Boolean> = TrackHelper.addWayPointToTrack(this@TrackerService, track, currentBestLocation, locationAccuracyThreshold, resumed)
// get track from result
track = result.first
// check if waypoint was successfully added (= result.second)
if (resumed && result.second) {
// reset resumed flag, if necessary
resumed = false
}
2020-01-02 17:00:37 +00:00
// update notification
displayNotification()
// save temp track using GlobalScope.launch = fire & forget (no return value from save)
GlobalScope.launch { FileHelper.saveTempTrackSuspended(this@TrackerService, track) }
// re-run this in 10 seconds
handler.postDelayed(this, Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL)
}
}
/*
* End of declaration
*/
}