Foreground services let you asynchronously perform operations that are noticeable to the user
Here are the main files as they should look.
-
AndroidManifest.xml
-
activity_main.xml
-
MainActivity.kt
-
SeparateProcessServiceExample.kt
-
IntentGlobalActions.kt
-
Below, I tried to explain everything line by line.
\SeparateProcessServiceExample\app\src\main\
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SeparateProcessServiceExample">
<service
android:name=".SeparateProcessServiceExample"
android:enabled="true"
android:exported="false"
android:permission="android.permission.FOREGROUND_SERVICE"
android:foregroundServiceType="dataSync"
android:process=":SeparateProcessServiceExample" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\SeparateProcessServiceExample\app\src\main\res\layout\
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<ScrollView
android:id="@+id/scrollLog"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
android:scrollbars="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/tvLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Log:"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:typeface="monospace"
android:gravity="top|start"
android:background="#EEEEEE"
android:padding="8dp" />
</ScrollView>
<Button
android:id="@+id/btnStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start"
android:layout_marginBottom="64dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\
MainActivity.kt:
package com.milosev.separateProcessServiceExample
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.milosev.separateProcessServiceExample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val maxLines = 500
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val msg = intent?.getStringExtra("message")
appendLog("Receiver: Got: $msg")
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
100
)
}
val filter = IntentFilter(IntentGlobalActions.TEST_MESSAGE_ACTION)
registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED)
binding.btnStart.setOnClickListener {
val intent = Intent(this, SeparateProcessServiceExample::class.java)
intent.action = IntentGlobalActions.START_FOREGROUND_SERVICE
startForegroundService(intent)
}
}
fun appendLog(message: String) {
runOnUiThread {
binding.tvLog.append("\n$message")
val lines = binding.tvLog.text.split("\n")
if (lines.size > maxLines) {
val trimmed = lines.takeLast(maxLines).joinToString("\n")
binding.tvLog.text = trimmed
}
binding.scrollLog.post {
binding.scrollLog.fullScroll(View.FOCUS_DOWN)
}
}
}
}
\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\
SeparateProcessServiceExample.kt:
package com.milosev.separateProcessServiceExample
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.Color
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SeparateProcessServiceExample : Service(), CoroutineScope by MainScope() {
private var job: Job? = null
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
IntentGlobalActions.START_FOREGROUND_SERVICE -> {
val channelId = createNotificationChannel("my_service", "My Background Service")
val notificationBuilder = NotificationCompat.Builder(this, channelId)
val notification = notificationBuilder.setOngoing(true)
.setContentTitle("test")
.setContentText("test")
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(1)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
startForeground(101, notification)
var messageNumber = 0
job = launch {
while(true) {
messageNumber++
val intent = Intent(IntentGlobalActions.TEST_MESSAGE_ACTION)
intent.putExtra("message", "Message number: $messageNumber")
intent.setPackage(packageName)
sendBroadcast(intent)
delay(1_000)
}
}
}
}
return START_STICKY
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String {
val chan = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT
)
chan.lightColor = Color.RED
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
}
\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\
IntentGlobalActions.kt:
package com.milosev.separateProcessServiceExample
object IntentGlobalActions {
const val TEST_MESSAGE_ACTION = "com.milosev.TEST_MESSAGE_ACTION"
const val START_FOREGROUND_SERVICE = "startService"
}
---
Since LocalBroadcastManager is
deprecated, here are examples for creating a separate process Service and sending messages from the process to the UI.
First I created new project with "Empty Views Activity":
Then I have added new service and name it "
SeparateProcessServiceExample", please note that
later I will derive it from
MainScope:
I have disabled exported, since I don't want to share messages between other applications:
In "\SeparateProcessServiceExample\app\src\main\AndroidManifest.xml" I have added permissions for FOREGROUND_SERVICE, and since later I want to display icon in notification when my service starts, I will also need POST_NOTIFICATIONS, also I need FOREGROUND_SERVICE_DATA_SYNC permission because I will want to start with "
startForeground" method:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Also in AndroidManifest.xml I have added service under node application. "foregroundServiceType" I need because of the error:
To call Service.startForeground(), the <service> element of manifest file must have the foregroundServiceType attribute specified
<service
android:name=".SeparateProcessServiceExample"
android:enabled="true"
android:exported="false"
android:permission="android.permission.FOREGROUND_SERVICE"
android:foregroundServiceType="dataSync"
android:process=":SeparateProcessServiceExample" />
So my "\SeparateProcessServiceExample\app\src\main\AndroidManifest.xml" looks like:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SeparateProcessServiceExample">
<service
android:name=".SeparateProcessServiceExample"
android:enabled="true"
android:exported="false"
android:permission="android.permission.FOREGROUND_SERVICE"
android:foregroundServiceType="dataSync"
android:process=":SeparateProcessServiceExample" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Now I have addded new object "IntentGlobalActions", for better readibility, which I will use to define actions for intent:
object IntentGlobalActions {
const val TEST_MESSAGE_ACTION = "com.milosev.TEST_MESSAGE_ACTION"
const val START_FOREGROUND_SERVICE = "startService"
}
My "\SeparateProcessServiceExample\app\src\main\res\layout\activity_main.xml" looks like:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<ScrollView
android:id="@+id/scrollLog"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
android:scrollbars="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/tvLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Log:"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:typeface="monospace"
android:gravity="top|start"
android:background="#EEEEEE"
android:padding="8dp" />
</ScrollView>
<Button
android:id="@+id/btnStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start"
android:layout_marginBottom="64dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Ask user to allow notification permission:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
100
)
}
}
Setup
View Binding:
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
but don't forget to enable viewBinding in your "\SeparateProcessServiceExample\app\build.gradle.kts":
buildFeatures {
viewBinding = true
}
In "\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\SeparateProcessServiceExample.kt" first override onStartCommand and filter pro intent messages defined in IntentGlobalActions, and return
START_STICKY:
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
IntentGlobalActions.START_FOREGROUND_SERVICE -> {
}
}
return START_STICKY
}
create notification:
val channelId = createNotificationChannel("my_service", "My Background Service")
val notificationBuilder = NotificationCompat.Builder(this, channelId)
val notification = notificationBuilder.setOngoing(true)
.setContentTitle("test")
.setContentText("test")
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(1)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
Where the method createNotificationChannel looks like:
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String {
val chan = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT
)
chan.lightColor = Color.RED
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
Start service:
startForeground(101, notification)
Next, I want to have an endles task, from which I want to send messages like:
private var job: Job? = null
...
job = launch {
while(true) {
delay(10_000)
}
}
In order to use launch my SeparateProcessServiceExample to derive from
CoroutineScope by MainScope(), so now it looks like:
class SeparateProcessServiceExample : Service(), CoroutineScope by MainScope() {
Message sending from service looks like:
val intent = Intent(IntentGlobalActions.TEST_MESSAGE_ACTION)
intent.putExtra("message", "Message number: $messageNumber")
intent.setPackage(packageName)
sendBroadcast(intent)
Now, back to "\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\MainActivity.kt", create reciever like:
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val msg = intent?.getStringExtra("message")
appendLog("Receiver: Got: $msg")
}
}
Register it:
val filter = IntentFilter(IntentGlobalActions.TEST_MESSAGE_ACTION)
registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED)
Where the method appendLog looks like:
fun appendLog(message: String) {
runOnUiThread {
binding.tvLog.append("\n$message")
val lines = binding.tvLog.text.split("\n")
if (lines.size > maxLines) {
val trimmed = lines.takeLast(maxLines).joinToString("\n")
binding.tvLog.text = trimmed
}
binding.scrollLog.post {
binding.scrollLog.fullScroll(View.FOCUS_DOWN)
}
}
}
To Stop a service from \SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\
MainActivity.kt I have sent STOP_FOREGROUND_SERVICE:
val intent = Intent(this, SeparateProcessServiceExample::class.java)
intent.action = IntentGlobalActions.STOP_FOREGROUND_SERVICE
startForegroundService(intent)
and in \SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\
SeparateProcessServiceExample.kt:
IntentGlobalActions.STOP_FOREGROUND_SERVICE -> {
job?.cancel()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelfResult(startId)
}
Example download from
here.