Skip to content

Instantly share code, notes, and snippets.

@LaurieScheepers
Last active March 2, 2022 11:08
Show Gist options
  • Save LaurieScheepers/c34f8bbf22ceb2297615a303a9b2d247 to your computer and use it in GitHub Desktop.
Save LaurieScheepers/c34f8bbf22ceb2297615a303a9b2d247 to your computer and use it in GitHub Desktop.
A Service that tracks the user's location for a minute (configurable), averages it and sends it off to a server
package com.company.app.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import java.util.*
import javax.inject.Inject
/**
* A foreground service that tracks a user's location for a configurable time,
* averages the location results, and then updates a server with the coordinates.
*
* Created by Laurie on 2021/10/12.
*/
const val NOTIFICATION_ID = 1
const val NOTIFICATION_ID_OREO = 2
const val NOTIFICATION_CHANNEL_ID = BuildConfig.APPLICATION_ID
const val NOTIFICATION_CHANNEL_NAME = "Location Service Notifier"
class LocationService : Service(), LocationListener {
private var locationManager: LocationManager? = null
private var location: Location? = null
@Inject
lateinit var authApi: AuthApi
@Inject
lateinit var authStorage: AuthStorage
private var notificationManager: NotificationManager? = null
private var timer: Timer? = null
private var timerTask: TimerTask? = null
private val listOfLocations: ArrayList<Pair<Double?, Double?>> = arrayListOf() // first: lat, second: long
override fun onCreate() {
super.onCreate()
// Never have more than one location service
if (LocationUtil.isLocationServiceRunning(this)) {
Log.e("LocationService", "Trying to create another instance of LocationService")
return
}
Log.i("LocationService", "Starting Location Service")
MyApplication.getComponent().authComponent().inject(this)
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (locationManager != null) {
Log.i("LocationService", "This device supports Location")
} else {
Log.e("LocationService", "This device does not support location or GPS is turned off")
endSelf()
return
}
requestLocationUpdates()
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannelAndStartForegroundNotification() {
val channelName = NOTIFICATION_CHANNEL_NAME
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
channelName,
NotificationManager.IMPORTANCE_NONE
)
channel.lightColor = Color.BLUE
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager?.createNotificationChannel(channel)
val notification = createNotification()
startForeground(NOTIFICATION_ID_OREO, notification)
notificationManager?.notify(NOTIFICATION_ID_OREO, notification)
}
private fun createNotification(): Notification {
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationBuilder.setOngoing(true)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(getString(R.string.location_service_title))
.setContentText(getString(R.string.location_service_message))
.setPriority(NotificationManager.IMPORTANCE_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
} else {
notificationBuilder.setOngoing(true)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(getString(R.string.location_service_title))
.setContentText(getString(R.string.location_service_message))
.build()
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Log.i("LocationService", "Start command received for Location Service")
startForegroundNotification()
startTimerTask()
return START_STICKY
}
/**
* This timer task sends a location update to the server once every 10 seconds for one minute
* of location tracking and then stops itself and this foreground service
*/
private fun startTimerTask() {
if (timer == null && timerTask == null) {
timer = Timer()
timerTask = object : TimerTask() {
override fun run() {
if (counter >= 6) { // Will stop after a minute (6 * 10 seconds - see below)
Log.d("LocationService", "We are finished tracking location for now" +
" - averaging location and stopping service.")
val averageLocation = LocationUtil.averageLocation(listOfLocations)
// Now update the server (even if location is null - server wants to know which vendors location requests failed)
LocationUtil.sendCoordinatesToServer(
authApi,
DeviceUtil.getDeviceImei(applicationContext),
averageLocation
)
// STOP
endSelf()
return
}
incrementCount()
if (location != null) {
Log.d(
"Adding Location",
location?.latitude.toString() + ": " + location?.longitude.toString()
+ " Count " + counter.toString()
)
// Add to list of locations
location?.let { listOfLocations.add(Pair(it.latitude, it.longitude)) }
// Also save in persistent storage
LocationUtil.saveLastKnownLocation(authStorage, location)
}
}
}
timer?.schedule(
timerTask,
0,
10000L // repeats every 10 seconds
)
} else {
Log.e("LocationService", "Trying to create another timer - stopping")
stopTimerTask()
restartSelf()
}
}
/**
* This is the main method for this service to end itself
*/
private fun endSelf() {
destroySelf()
stopForegroundNotification()
stopTimerTask()
stopSelf() // stop service
}
private fun stopTimerTask() {
resetCount()
// Cancel all timer-related tasks and cleanup
timer?.cancel()
timer?.purge()
timerTask?.cancel()
timer = null
timerTask = null
}
private fun startForegroundNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannelAndStartForegroundNotification()
} else {
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager?.notify(NOTIFICATION_ID, notification)
}
}
private fun stopForegroundNotification() {
stopForeground(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager?.cancel(NOTIFICATION_ID_OREO)
} else {
notificationManager?.cancel(NOTIFICATION_ID)
}
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
Log.i("LocationService", "onDestroy() called")
stopForegroundNotification()
removeLocationListener()
stopTimerTask()
// Only send the broadcast if this service was stopped by something other than this service
if (!selfDestroy) {
Log.i("LocationService", "Service stopped unexpectedly. Sending broadcast to restart service.")
LocationUtil.sendRestartServiceBroadcast(this)
}
}
private fun requestLocationUpdates() {
selfDestroy = false
if (!LocationUtil.isLocationEnabled(this)) {
Log.e("LocationService", "This device does not support location or GPS is turned off")
endSelf()
return
}
Log.i("LocationService", "Enqueuing location updates")
if (LocationUtil.isLocationPermissionEnabled(this)) {
try {
locationManager?.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, this)
// Force an update with the last location, if available.
val lastLocation = LocationUtil.retrieveLastKnownLocation(this)
if (lastLocation != null) {
onLocationChanged(lastLocation)
} else {
Log.e("LocationService", "Failed to request last known location")
}
} catch (e: Exception) {
Log.e("LocationService", "Failed to request location", e)
}
} else {
Log.e("LocationService", "Unable to request location as permissions have been denied")
}
}
private fun removeLocationListener() {
if (locationManager == null) {
return
}
locationManager?.removeUpdates(this)
locationManager = null
Log.d("LocationService", "Location listener removed")
}
override fun onLocationChanged(location: Location) {
// We only want to temporarily store the location here
// The logic of sending updates to the server is done in the timer task
this.location = location
}
/**
* Have to override these LocationListener methods below:
* see https://stackoverflow.com/questions/64638260/android-locationlistener-abstractmethoderror-on-onstatuschanged-and-onproviderd
*/
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { }
override fun onProviderEnabled(provider: String) {
// Restart the service if GPS is turned on again
Log.d("LocationService", "GPS has been turned on, restarting service")
restartSelf()
LocationUtil.sendRestartServiceBroadcast(this)
}
override fun onProviderDisabled(provider: String) {
// End service if GPS has been turned off
Log.e("LocationService", "GPS has been turned off")
stopTimerTask()
stopForegroundNotification()
restartSelf() // Prepare for restarting
}
companion object {
var counter = 0
var selfDestroy = false
fun incrementCount() {
counter++
}
fun resetCount() {
counter = 0
}
fun destroySelf() {
selfDestroy = true
}
fun restartSelf() {
selfDestroy = false
}
fun endFromAnywhere(service: LocationService) {
service.endSelf()
}
fun isLocationTimerRunning(service: LocationService): Boolean {
return service.timerTask != null && service.timer != null
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment