The curious case of Foreground service notifications
The curious case of Foreground service notifications
Ok, let’s start with looking at some history.
A background on background work in Android
When I first got into Android development, background services were kinda unrestricted
Because the Services API is so powerful and being able to do things in the background while persisting the app’s process can actually affect the device’s performance due to contention (Memory, CPU) and usage (Battery), developers certainly have a responsibility to use services wisely.
One more thing to note is progressive restrictions from Android 8.0’s background execution limits
My favourite API to do long running background processing in Android is thusly WorkManager
Foreground services and notifications
Of course Android supports long running operations that the user explicitly requests. These are run by posting status bar notifications and using foreground services
The rationale behind this is that
The user has explicitly requested a long running operation that can persist outside of the app being killed.
The notification is visible to the user throughout the duration of this long running operation.
So, I suppose the question here to ask is what would happen if the user has explicitly turned off ALL notifications in the app’s settings?
Let’s find out by creating a sample app that does this.
- First we create a Service and declare it in the app’s
AndroidManifest.xml
1
2
3
4
<service
android:name=".SomeService"
android:enabled="true"
android:exported="false" />
- Create
SomeService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class SomeService : Service() {
companion object {
const val CHANNEL_NAME = "serviceChannel"
const val CHANNEL_ID = "channelId"
const val TITLE = "SomeService up"
const val MESSAGE = "SomeService is now running"
val targetActivity = HomeActivity::class.java
const val NOTIFICATION_ID = 0
}
override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val target = Intent(this, targetActivity)
val targetIntent =
PendingIntent.getActivity(this, 1, target, PendingIntent.FLAG_UPDATE_CURRENT)
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(TITLE)
.setContentText(MESSAGE)
.setNotificationSilent()
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(targetIntent)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
}
if (Build.VERSION.SDK_INT >= 29) {
startForeground(startId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE)
} else {
startForeground(startId, notification)
}
serviceStarted = true
return START_NOT_STICKY
}
override fun stopService(name: Intent?): Boolean {
serviceStarted = false
return super.stopService(name)
}
}
ServiceNotifier
is aSingleton
that sets/unsets theserviceStarted
flag. This is just for us to debugSomeService
when notifications are turned off.
1
2
3
object ServiceStartedNotifier {
var serviceStarted = false
}
HomeActivity
is just the launcher activity.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (!serviceStarted) {
val serviceIntent = Intent(this, SomeService::class.java)
startService(serviceIntent)
}
// poll for `SomeService` being up/ down.
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
while (true) {
delay(2000)
withContext(Dispatchers.Main) {
txt_status.text = if (serviceStarted) "Service is up" else "Service is down"
}
}
}
}
// Not strictly required, but saves me from killing the service manually.
override fun onDestroy() {
super.onDestroy()
serviceStarted = false
stopService(Intent(this, SomeService::class.java))
}
activity_main.xml
is just an xml layout. I should probably have written this in Jetpack Compose!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".HomeActivity">
<TextView
android:id="@+id/txt_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:textIsSelectable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Ok, now for the big reveal
That’s right. For foreground services, if the user stops showing notifications, this just stops the service from posting notifications! However, the service and its processing still seems to be working as normal