Ошибка startForeground после обновления до Android 8.1: как исправить?
Причина ошибки и изменения в Android 8.0+
Проблема связана с тем, что начиная с Android 8.0 (API 26) система требует обязательного вызова Service.startForeground() в течение очень короткого времени (буквально несколько секунд) после вызова Context.startForegroundService(). Если вызов startForeground() не происходит вовремя, Android убивает сервис и генерирует исключение ForegroundServiceDidNotStartInTimeException.
В вашем коде проблема в том, что вы вызываете startForeground() в onStartCommand(), но система может вызвать этот метод с задержкой или не вызвать вовсе, если сервис был создан через onCreate() и потом только onStartCommand(). На Android 8.1 тайм-аут стал ещё короче, и даже небольшая задержка (например, из-за инициализации канала уведомлений или других операций) приводит к ошибке.
Правильное решение на Kotlin
Нужно вызывать startForeground() как можно раньше — сразу после создания сервиса, а не ждать onStartCommand(). Лучший способ — сделать это в onCreate() или в самом начале onStartCommand(), но обязательно синхронно и без задержек.
Вот исправленный код:
class LocationService : Service() {
private val NOTIFICATION_ID = 1
private val CHANNEL_ID = "location_channel"
override fun onCreate() {
super.onCreate()
createNotificationChannel()
// Вызываем startForeground сразу при создании сервиса
val notification = buildNotification()
startForeground(NOTIFICATION_ID, notification)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Здесь уже ничего не делаем с startForeground, он уже вызван
// Но можно обновить уведомление, если нужно
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Сервис местоположения",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Уведомление для фонового сервиса"
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private fun buildNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Сервис активен")
.setContentText("Отслеживание местоположения")
.setSmallIcon(R.drawable.ic_location)
.setOngoing(true)
.build()
}
}
Важные моменты:
- Канал уведомлений должен быть создан до вызова
startForeground(). В примере выше он создаётся вonCreate()перед уведомлением. - Не выполняйте длительные операции (сеть, база данных) между вызовом
startForegroundService()иstartForeground(). - Всегда используйте
NotificationCompat.Builderдля обратной совместимости. - Если вам нужно обновить уведомление позже, используйте
NotificationManager.notify()с тем же ID.
Как правильно запускать сервис:
val intent = Intent(this, LocationService::class.java)
ContextCompat.startForegroundService(this, intent) // единый способ для всех версий
Функция ContextCompat.startForegroundService() сама решает, какой метод вызывать в зависимости от версии Android.
Альтернативный подход: использование JobIntentService или WorkManager
Если ваша задача не требует постоянного сервиса (например, вы просто отправляете данные на сервер раз в несколько минут), рассмотрите использование JobIntentService или WorkManager. Эти компоненты лучше адаптированы для фоновой работы на Android 8+ и не требуют вызова startForeground().
Пример с JobIntentService:
class LocationJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
// Ваша фоновая работа здесь
// Не нужно вызывать startForeground()
}
companion object {
fun enqueueWork(context: Context, intent: Intent) {
enqueueWork(context, LocationJobService::class.java, JOB_ID, intent)
}
}
}
Запуск:
LocationJobService.enqueueWork(this, Intent(this, LocationJobService::class.java))
Этот подход автоматически обрабатывает ограничения Android на фоновые сервисы и не требует управления уведомлениями вручную. Однако помните: если ваше приложение должно показывать уведомление постоянно (например, для трекера), то startForeground() — единственный правильный путь.