Last active
March 2, 2022 11:08
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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