Overview
LocationService is an Android foreground service designed to continuously track the user’s location, handle GPS and internet outages, detect mock locations, and send location data to a server using Socket.IO. The service is resilient to process death and network issues and includes a periodic watchdog via WorkManager to restart itself if needed.
Components
1. Service Initialization
override fun onCreate() {
super.onCreate()
log("Service onCreate called")
setupSocket()
acquireWakeLock()
registerReceivers()
schedulePeriodicWork()
}
- Called when the service is first created.
- Initializes the socket connection, acquires a partial wake lock (to keep CPU on), registers receivers (if any), and schedules periodic monitoring via WorkManager.
private fun setupSocket() {
try {
socket = IO.socket("https://route.getfieldy.com")
socket.connect()
} catch (e: URISyntaxException) {
log("Socket error: ${e.message}")
}
}
- Initializes and connects the
Socket.IOinstance to the backend server.
2. Service Lifecycle
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val action = intent?.action
log("onStartCommand called. Intent action: $action")
if (action == null && !isRunning) {
startTracking()
} else {
when (action) {
ACTION_START -> startTracking()
ACTION_STOP -> {
prefs.edit().putBoolean("IS_RUNNING", false).apply()
stopTracking()
}
}
}
return START_STICKY
}
- Handles service start and stop commands.
- Ensures the service continues to run (START_STICKY) even if the system kills it.
3. Location Tracking Setup
private fun startTracking() {
isRunning = true
prefs.edit().putBoolean("IS_RUNNING", true).apply()
- Marks the tracking as active in SharedPreferences.
val locationDataString = prefs.getString("LOCDATA", "_")
if (locationDataString != "_") {
val locData = JSONObject(locationDataString ?: "{}")
userName = locData.getString("name")
userImageUrl = locData.getString("image")
tenantId = locData.getString("tenant")
technicianId = locData.getString("technician")
metaDataId = locData.getString("metaId")
metaDataType = locData.getString("metaType")
}
- Loads stored user/technician and tracking metadata from preferences.
setupNotification()
locationClient = LocationServices.getFusedLocationProviderClient(this)
- Builds the notification and initializes the FusedLocationProviderClient.
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000L)
.setMinUpdateIntervalMillis(5000L)
.setWaitForAccurateLocation(true)
.setGranularity(Granularity.GRANULARITY_FINE)
.setMinUpdateDistanceMeters(5f)
.build()
- Configures a high-accuracy location request with a 5-second interval and 5-meter minimum movement.
locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.locations.forEach { location ->
processLocation(location, result)
}
}
}
- Defines how to handle new location updates.
locationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
}
- Starts receiving location updates from the fused location provider.
4. Location Handling and Emission via Socket
private fun processLocation(location: Location, result: LocationResult) {
val isMockLocation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
location.isMock
} else {
location.isFromMockProvider
}
- Determines if the current location is mocked (fake GPS).
batteryPercentage = getBatteryPercentage()
- Retrieves the current device battery percentage.
val message = when {
batteryPercentage < 20 -> "⚠️ Low battery: $batteryPercentage%"
isMockLocation -> "⚠️ Mock location detected."
else -> "Tracking location... ${location.latitude}, ${location.longitude}"
}
- Decides what message to show based on mock/battery status.
notification = buildNotification(message)
notificationManager.notify(1, notification)
- Updates the foreground notification dynamically.
prepareData(location, result)
}
- Passes data to be structured and sent.
private fun prepareData(location: Location, result: LocationResult) {
val isMockLocation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) location.isMock else location.isFromMockProvider
val isInternetAvailable = isInternetAvailable()
- Checks mock location and current network availability.
val locationData = buildLocationData(
location.latitude,
location.longitude,
0.0,
batteryPercentage,
event = if (isMockLocation) "location_mocked"
else if (isGPSoutage) "gps_outage"
else if (!isInternetAvailable) "internet_disconnected"
else if (metaDataType == "availability_on_traveling") "availability_on_traveling"
else "traveling",
mocked = isMockLocation,
accuracy = location.accuracy,
speed = location.speed,
bearingDiff = location.bearingAccuracyDegrees.toString(),
locationResult = result
)
- Builds a JSON object with the current location context.
if (isInternetAvailable) {
sendToSocket(locationData)
prefs.getString("OFFLINE_DATA", null)?.let {
sendToSocket(JSONObject(it))
val recoveryData = JSONObject(locationData.toString()).apply {
put("event", "internet_resumed")
}
sendToSocket(recoveryData)
prefs.edit().remove("OFFLINE_DATA").apply()
}
} else {
prefs.edit().putString("OFFLINE_DATA", locationData.toString()).apply()
}
}
- Sends live data via socket if online, or stores it offline if not.
- If offline data exists when internet resumes, it also gets sent.
5. Send to Server via Socket.IO
private fun sendToSocket(locationData: JSONObject) {
if (!socket.connected()) {
try {
socket.connect()
} catch (e: Exception) {
log("Socket reconnect failed: ${e.message}")
}
}
when (metaDataType) {
"travel_history_full_day_tracking" -> socket.emit("travel_history_full_day_tracking", locationData)
"availability_on_traveling" -> socket.emit("current_location", locationData)
else -> socket.emit("travel_history", locationData)
}
}
- Reconnects socket if disconnected.
- Emits different event types depending on the tracking mode (
metaDataType).
Monitoring & Utilities
6. Background Watchdog
class ServiceMonitorWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result {
val prefs = applicationContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
if (!prefs.getBoolean("IS_RUNNING", false)) {
val intent = Intent(applicationContext, LocationServiceV2::class.java).apply {
action = LocationServiceV2.ACTION_START
}
ContextCompat.startForegroundService(applicationContext, intent)
}
return Result.success()
}
}
- Periodic WorkManager worker that checks if the service is still running.
- If stopped, it restarts the service using
ContextCompat.startForegroundService.
Conclusion
This version of LocationService uses Socket.IO for real-time transmission of location data, suitable for applications that need low-latency live tracking, such as fieldwork or logistics. Socket reconnection and offline buffer handling are included to ensure robustness.
