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")
}
}
}
}
}
}
}
