Avatar ·

Ошибка startForeground после обновления до Android 8.1: как исправить?

Всем привет! Я разрабатываю приложение на Kotlin, которое использует фоновый сервис для отслеживания местоположения. После обновления устройства до Android 8.1 (Oreo) мой сервис перестал работать и падает с ошибкой `Context.startForegroundService() did not then call Service.startForeground()`. Вот мой код сервиса: ```kotlin class LocationService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification = NotificationCompat.Builder(this, "channel_id") .setContentTitle("Сервис активен") .setContentText("Отслеживание местоположения") .setSmallIcon(R.drawable.ic_location) .build() startForeground(1, notification) return START_STICKY } override fun onBind(intent: Intent?): IBinder? = null } ``` И запускаю его так: ```kotlin val intent = Intent(this, LocationService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent) } else { startService(intent) } ``` Ошибка возникает не всегда, а только когда приложение находится в фоне или устройство заблокировано. Я уже создал канал уведомлений в Application.onCreate(). Что я делаю не так? Почему Android 8.1 стал таким строгим? Пожалуйста, объясните причину и дайте рабочий код на Kotlin. <<>>

Причина ошибки и изменения в 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() — единственный правильный путь.