Foreground service getting killed

Viewed 1212

I have a service, that updates my backend from time to time, with the newest location of the user, the problem is, some users are reporting that the Foreground notification sometimes disappears, but it doesn't happen to all the users, there might be users using the app 8 hours a day, and it doesn't disappear, but no crash appears on Crashlytics.

Here is my ServiceClass

package com.xxxxxxx.xxxxxxxx.service

import android.annotation.SuppressLint
import android.app.Notification
import android.app.Notification.FLAG_NO_CLEAR
import android.app.Notification.FLAG_ONGOING_EVENT
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.Service
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
import android.graphics.Color
import android.location.Location
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
import android.os.IBinder
import android.util.Base64
import android.util.Base64.DEFAULT
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.model.LatLng
import com.xxxx.commons.XXXXXXCommons
import com.xxxxxxx.xxxxxxxx.R
import com.xxxxxxx.xxxxxxxx.di.DataSourceModule
import com.xxxxxxx.xxxxxxxx.domain.GetAppStateUseCase.Companion.LOCATION_DELIMITER
import com.xxxxxxx.xxxxxxxx.model.local.CachedLocation
import com.xxxxxxx.xxxxxxxx.model.local.DriverLocation
import com.xxxxxxx.xxxxxxxx.utils.LOCATION_PERMISSIONS
import com.xxxxxxx.xxxxxxxx.utils.OnGoingTrip
import com.xxxxxxx.xxxxxxxx.utils.checkPermissions
import com.xxxxxxx.xxxxxxxx.view.activity.SplashActivity
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.text.Charsets.UTF_8

class LocationTrackingService : Service() {

    private companion object {
        const val NOTIFICATION_ID = 856
        const val FOREGROUND_SERVICE_ID = 529
        const val LOCATION_UPDATE_INTERVAL = 10000L
        const val LOCATION_UPDATE_FASTEST_INTERVAL = 5000L
    }

    private val updateJob: Job = Job()
    private val errorHandler = CoroutineExceptionHandler { _, throwable ->
        XXXXXXCommons.crashHandler?.log(throwable)
    }
    private val locationTrackingCoroutineScope =
        CoroutineScope(Dispatchers.IO + updateJob + errorHandler)

    private val rideApiService by lazy { DataSourceModule.provideRideApiService() }
    private val locationDao by lazy { DataSourceModule.provideLocationDao(XXXXXXCommons.provideApplicationContext()) }

    private val locationRequest = LocationRequest().apply {
        interval = LOCATION_UPDATE_INTERVAL
        fastestInterval = LOCATION_UPDATE_FASTEST_INTERVAL
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY
    }

    private val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) = onLocationReceived(result)
    }

    @SuppressLint("MissingPermission")
    private fun requestLocationUpdates() {
        if (checkPermissions(*LOCATION_PERMISSIONS)) {
            LocationServices.getFusedLocationProviderClient(this)
                .requestLocationUpdates(locationRequest, callback, null)
        }
    }

    private fun onLocationReceived(result: LocationResult?) = result?.lastLocation?.run {
        postDriverLocation(this)
    } ?: Unit

    private fun postDriverLocation(location: Location) {
        val driverLocation = DriverLocation(
            location.latitude.toFloat(),
            location.longitude.toFloat(),
            location.bearing,
            OnGoingTrip.rideData.rideId,
            OnGoingTrip.orderData.orderId
        )
        OnGoingTrip.currentLocation = LatLng(location.latitude, location.longitude)
        OnGoingTrip.reportLocationUpdate()
        locationTrackingCoroutineScope.launch(Dispatchers.IO) {
            cacheDriverLocation(driverLocation)
            rideApiService.postCurrentDriverLocation(driverLocation)
        }
    }

    private suspend fun cacheDriverLocation(driverLocation: DriverLocation) {
        val bytes = "${driverLocation.latitude}$LOCATION_DELIMITER${driverLocation.longitude}"
            .toByteArray(UTF_8)
        val safeLocation = Base64.encode(bytes, DEFAULT).toString(UTF_8)
        locationDao.createOrUpdate(CachedLocation(location = safeLocation))
    }

    override fun onBind(intent: Intent): IBinder? = null

    override fun onCreate() {
        super.onCreate()
        buildNotification(
            if (SDK_INT >= O) {
                createNotificationChannel(getString(R.string.app_name))
            } else {
                NOTIFICATION_ID.toString()
            }
        )
        requestLocationUpdates()
    }

    override fun onDestroy() {
        super.onDestroy()
        if (locationTrackingCoroutineScope.isActive) {
            locationTrackingCoroutineScope.cancel()
        }
        LocationServices.getFusedLocationProviderClient(this).removeLocationUpdates(callback)
        (getSystemService(NOTIFICATION_SERVICE) as? NotificationManager)?.cancel(NOTIFICATION_ID)
    }

    @RequiresApi(O)
    private fun createNotificationChannel(channelName: String): String {
        val chan = NotificationChannel(
            NOTIFICATION_ID.toString(),
            channelName,
            NotificationManager.IMPORTANCE_NONE
        )
        chan.lightColor = Color.CYAN
        chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
        val service = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        service.createNotificationChannel(chan)
        return NOTIFICATION_ID.toString()
    }

    private fun buildNotification(channelId: String) {
        val ctx = this
        (getSystemService(NOTIFICATION_SERVICE) as? NotificationManager)?.run {
            val intent = Intent(ctx, SplashActivity::class.java).apply {
                flags = FLAG_ACTIVITY_CLEAR_TOP or FLAG_ACTIVITY_SINGLE_TOP
            }
            val resultPendingIntent =
                PendingIntent.getActivity(ctx, 0, intent, FLAG_UPDATE_CURRENT)
            val notification = NotificationCompat.Builder(ctx, channelId)
                .setSmallIcon(R.drawable.ic_xxxxxx_icon_background)
                .setColor(ContextCompat.getColor(ctx, R.color.xxxxxxTurquoise))
                .setContentTitle(ctx.getString(R.string.app_name))
                .setOngoing(true)
                .setContentText(ctx.getString(R.string.usage_message))
                .setContentIntent(resultPendingIntent)
                .build().apply { flags = FLAG_ONGOING_EVENT or FLAG_NO_CLEAR }
            startForeground(FOREGROUND_SERVICE_ID, notification)
        }
    }
}

Here is how I start the service:

startService(Intent(this, LocationTrackingService::class.java));

Here are the permissions I have and how I declared it on the AndroidManifest.xml:

<service
            android:name=".service.LocationTrackingService"
            android:enabled="true"
            android:exported="true" />

<uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

Edit

Most of the devices run Android 9 and 10, a few from 5 to 8, and almost no 11

More information I would be happy to share.

2 Answers

There have been several latest restrictions with Android 11. It's difficult to provide an exact solution. Here is couple of things you should recheck that might resolve your problem:

  • Disable battery optimization for your app in the device.
  • Check if the device is not operating in doze mode.
  • Specify android:foregroundServiceType in manifest .

There are a couple of things here. I cannot see <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> in your manifest. Also make sure about foregroundServiceType="location" in your service attributes inside manifest. In addition, if you have a foreground service for location, ACCESS_BACKGROUND_LOCATION is not required. On the other hand, Xiaomi devices will kill foreground services when user closes the application from app switcher (Xiaomi is the worst) and you should ask Xiaomi users to manually exclude your application from being killed in battery saver or extra permissions in app details (and even if they do that, still there won't be a guarantee, also different MiUI versions have different rules) Moreover, star your service using ContextCompat.startForegroundService and in your service's onStartCommand callstartForeground

Related