So, I've been experimenting with the new Android emulators (namely Android 13 or Android Tiramisu, API 33) and on an app, I need a foreground service.
The app's target SDK is currently 33.
Due to the notification requirement, I've added the android.permission.POST_NOTIFICATIONS
to the manifest. And, because it is also a runtime permission, I'm asking the permission after the app is opened.
If, user denies the permission, but tries to perform a task that involves a foreground service after starting it with startForegroundService
, upon calling startForeground
from my service, I get a crash:
android.app.RemoteServiceException$CannotPostForegroundServiceNotificationException: Bad notification for startForeground
at android.app.ActivityThread.throwRemoteServiceException(ActivityThread.java:1983)
at android.app.ActivityThread.-$$Nest$mthrowRemoteServiceException(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2242)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7898)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Notice the exception name: CannotPostForegroundServiceNotificationException. So apparently the system effectively prevents from me to post a foreground service notification. This happens after the startForeground
call with a valid notification (up until API 32 anyway, and I did not see a change between API 32 and API 33 for constructing the notification itself, besides setOngoing(true)
which is something I'm already doing).
So, I checked if I can post notifications, using NotificationManager.areNotificationsEnabled()
. This returns false if user denies the permission, as expected. And the code now looks like this:
if (mNotificationManager.areNotificationsEnabled())
startForeground(123, mNotificationBuilder.build())
And, as expected, startForeground
does not get called. However, the task that needs to be executed might be long (about 2 minutes, maybe) and has to be in background, which cannot be performed in a job or through WorkManager
, and without calling the startForeground
, the app throws an exception after about 20 seconds with the following:
android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{ecbbdfb u0 com.example.android/.service.FgService}
at android.app.ActivityThread.generateForegroundServiceDidNotStartInTimeException(ActivityThread.java:2006)
at android.app.ActivityThread.throwRemoteServiceException(ActivityThread.java:1977)
at android.app.ActivityThread.-$$Nest$mthrowRemoteServiceException(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2242)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7898)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Caused by: android.app.StackTrace: Last startServiceCommon() call for this service was made here
at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1915)
at android.app.ContextImpl.startForegroundService(ContextImpl.java:1870)
at android.content.ContextWrapper.startForegroundService(ContextWrapper.java:822)
at com.example.android.MainActivity.startTaskWithFgService(MainActivity.kt:30)
at com.example.kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
I should note, the notification becomes visible normally if user accepts the notification permission, so this looks like a permission problem, and no crashes are observed.
Edit: The notification channel created should post silent notifications. So, this is how the notification channel gets created (and is also included while trying to post the notification):
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
withNotificationManager {
if (notificationChannels?.map { it.id }?.contains(SILENT_NOTIF_CHANNEL_ID) == true)
return@withNotificationManager
val channel = NotificationChannel(
SILENT_NOTIF_CHANNEL_ID,
getString(R.string.silent_notif_channel_name),
NotificationManager.IMPORTANCE_MIN).apply {
enableLights(false)
setShowBadge(false)
setSound(null, null)
description = getString(R.string.silent_notif_channel_desc)
vibrationPattern = null
lockscreenVisibility = Notification.VISIBILITY_SECRET
}
createNotificationChannel(channel)
}
}
From what I'm understanding, two things are happening:
- I can't start a foreground service using a notification because the permission was denied,
- I can't start a foreground service without a notification either because a notification is necessary.
These two conditions effectively removes foreground services functionality. This looks like an oversight to me.
Quote from Android 13 behavior changes notification permission (link):
"Apps don't need to request the POST_NOTIFICATIONS permission in order to launch a foreground service. However, apps must include a notification when they start a foreground service, just as they do on previous versions of Android."
So, my question is:
What should I do to execute a long task in background without a foreground service if the user denies the permission?
Thanks for reading the question and I appreciate any help, answer or a discussion.