Android Foreground Services With Notifications
Learn how to implement foreground services with notifications in Android that update as needed from coroutine.

In this post I will show you an example of how to implement Foreground Services With Notifications in Android.

Below are a few links of my previous posts regarding the implementation of Android Services, building notificationsΒ and requesting permissions in Android. The example below is greatly influenced by the content in these links and are a great point of reference.

Android Foreground Services

The example below implements the use of notifications to inform the user the Service is running. Android Foreground ServicesΒ require the use of a notification for the user while it is running. In order to add a notification, we need to request the Manifest.PERMISSION.POST_NOTIFICATIONΒ permission. While the Service is active, a coroutine will run a simple math function. If the total of the addition equation is over 95, the text content of the notification will be updated.

Notifications With Foreground Services

Add Required Permissions and Service to Android Manifest

First we will need to add the permissions for the foreground service and the post notifications. Then will will add the Service to the manifest.

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
        <service
            android:name=".CustomService"
            android:foregroundServiceType="shortService"
            android:exported="false" />

Create A Notification Builder

Now we will use the below object class to create the notifications. These will be built in the Service.

object MyNotificationBuilder {

    fun createForService(
        context: Context,
        channelId: String,
        title: String,
        content: String,
        channelName: String,
        channelDesc: String,
        importance: Int
    ) : Notification {
        val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationManager.createNotificationChannel(
                createNotificationChannel(
                    channelId,
                    channelName,
                    channelDesc,
                    importance
                )
            )
        }
        val pendingIntent: PendingIntent =
            PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)

        val builder = NotificationCompat.Builder(context, channelId)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setColor(Color(0xFFAAEDFF).toArgb())
            .setContentTitle(title)
            .setContentText(content)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
        return builder.build()
    }

    @SuppressLint("NewApi")
    private fun createNotificationChannel(
        channelId: String,
        channelName: String,
        channelDesc: String,
        importance: Int
    ): NotificationChannel {
        val channel = NotificationChannel(channelId, channelName, importance).apply {
            description = channelDesc
        }
        return channel
    }
}

Add A Permission Object Helper

Next we will use create a permission object to hold the state of the permission we need to request. This object will hold the state if the permission has been granted as well as if the rational dialog needs to be displayed to the user.

class RequestingPermission(
    var isGranted : Boolean = false,
    val permissionName : String = "",
    val message : String = "",
    var showRational : Boolean = false
)

Implement A Permission System With State

Our permission system is a Composable with a Switch. This Composable will handle the permission state and permission requests as well as a dialog for the rational statement leading the user to the Settings of the app.

The DisposableEffect is used to handle if the user navigates away from the application to the Settings then back. It will run the permission check again and update the state along with the UI.

I have chosen to add this as a separate Composable so this class is portable to other projects.Β 

@Composable
fun MyPermissionState(
    permissionState: List<RequestingPermission>,
    isGranted: Boolean,
    updateVisibility: (Boolean) -> Unit
) {

    val context = LocalContext.current

    var userEngaged by remember { mutableStateOf(false) }

    var showDialog by remember { mutableStateOf(false) }

    val requestPermissions =
        rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
            permissions.forEach { (name, state) ->
                when (name) {
                    permissionState[0].permissionName -> {
                        updateVisibility(state)
                    }
                }
            }
        }

    val goToSettingsRequest =
        rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->

        }

    val lifecycleOwner = LocalLifecycleOwner.current

    DisposableEffect(key1 = lifecycleOwner) {

        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME) {
                updateVisibility(context.checkSelfPermission(permissionState[0].permissionName) == PackageManager.PERMISSION_GRANTED)
            }

        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(15.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = "Enable Notifications"
            )
            Switch(checked = isGranted, onCheckedChange = {
                if(isGranted){
                    goToSettingsRequest.launch(
                        Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
                            Uri.fromParts("package", context.packageName, null)
                        ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    )
                    return@Switch
                }
                userEngaged = it
                val showRationaleState = ActivityCompat.shouldShowRequestPermissionRationale(
                    context as Activity,
                    permissionState[0].permissionName
                )
                Log.d("Show Rationale", showRationaleState.toString())
                if (!showRationaleState) {
                    requestPermissions.launch(arrayOf(permissionState[0].permissionName))
                } else {
                    userEngaged = true
                    showDialog = true
                }

            })
        }

    if (showDialog && userEngaged) {
        AlertDialog(
            onDismissRequest = {
                showDialog = false
                userEngaged = false
            },
            text = {
                Text(text = permissionState[0].message)
            },
            confirmButton = {
                TextButton(onClick = {
                    userEngaged = !userEngaged
                    goToSettingsRequest.launch(
                        Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
                            Uri.fromParts("package", context.packageName, null)
                        ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    )

                }) {
                    Text(text = "Open App Settings")
                }
            }
        )
    }
}

Create Foreground Service

Now we will build our custom foreground Service. In order to update the existing initial notification created from the coroutine, we just need to pass the name id to the NotificationManager.Β 

class CustomService : Service() {


        private val serviceCoroutine = CoroutineScope(Dispatchers.Default)
        private var serviceJob : Job? = null

        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            if(serviceJob == null){
                serviceJob = serviceCoroutine.launch {
                    delay(300)
                    while(isActive) {
                        val first = Math.floor(Math.random() * 100)
                        val second = Math.floor(Math.random() * 100)
                        val total = first + second
                        if(total > 95.0){
                            val notification = MyNotificationBuilder.createForService(
                                this@CustomService,
                                "test_channel",
                                "Testing Service",
                                "Value over 95 => ${total}",
                                "Foreground Service",
                                "Notification for Foreground Service",
                                NotificationCompat.PRIORITY_DEFAULT
                            )

                            startForeground(1234, notification)
                        }
                        logi("${first} + ${second} = ${total}")
                        delay(300)
                    }
                }
            }
            val notification = MyNotificationBuilder.createForService(
                this,
                "test_channel",
                "Testing Service",
                "Foreground Service...",
                "Foreground Service",
                "Notification for Foreground Service",
                NotificationCompat.PRIORITY_DEFAULT
            )

            startForeground(1234, notification)
            return START_NOT_STICKY
        }

        override fun onBind(intent: Intent?): IBinder? {
            return null
        }

        override fun onDestroy() {
            super.onDestroy()
            logi("onDestroy")
            serviceJob?.cancel()
            serviceJob = null
        }

}

fun Service.logi(message : String){
    Log.i("CustomService", message)
}

Create Android Activity

Finally we create our basic UI for the application.

class MainActivity : ComponentActivity() {

    var isRunning = false

    override fun onStop() {
        super.onStop()
        if(isRunning) {
            Intent(this@MainActivity, CustomService::class.java).apply {
                stopService(this)
            }

            isRunning = false
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {

            var isGranted by remember { mutableStateOf(false) }

            val permissionState = remember {
                mutableStateListOf(
                    RequestingPermission(
                        false,
                        permissionName = Manifest.permission.POST_NOTIFICATIONS,
                        message = "Please approve permission in order to receive notifications."
                    )
                )

            }

            SampleServiceTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Column(modifier = Modifier
                        .fillMaxSize()
                        .padding(innerPadding), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
                        MyPermissionState(permissionState, isGranted) {
                            isGranted = it
                        }
                        Button(
                            enabled = isGranted,
                            onClick = {
                                Intent(this@MainActivity, CustomService::class.java).apply {
                                    startForegroundService(this)
                                }
                                isRunning = true
                            }
                        ) {
                            Text(text = "Start")
                        }

                        Button(
                            enabled = isGranted,
                            onClick = {
                                Intent(this@MainActivity, CustomService::class.java).apply {
                                    stopService(this)
                                }
                                isRunning = false
                            }
                        ) {
                            Text(text = "Stop")
                        }
                    }
                }
            }
        }
    }
}