Уходит эпоха. В Android BroadcastReceiver больше не может сам принять бродкаст. Этот рассказ о попытке вернуть время вспять.

Начиная с Android Oreo, он же 8.0, он же API level 26, зарегистрированный в манифесте BroadcastReceiver может принимать только бродкасты из опубликованного в документации белого списка.

У меня есть служебная утилита, которая активизировалась только после получения бродкаста. Я ей посылал бродкасты с помощью adb shell am broadcast..., явно указывая компонент. Сегодня мне захотелось избавиться от имени компонента, потому что у этой утилиты появился еще один клиент - другое приложение. И я начал разбираться, почему же ресивер не получает бродкаст только по action, хотя должен бы.

Квест

Новость 1. Apps that target Android 8.0 or higher can no longer register broadcast receivers for implicit broadcasts in their manifest. An implicit broadcast is a broadcast that does not target that app specifically.

Но implicit бродкасты можно получать, если если ресиверы явно создать и зарегистрировать в коде.

Значит мне нужен Service, правильно?

Новость 2. When an app goes into the background, it has a window of several minutes in which it is still allowed to create and use services. At the end of that window, the app is considered to be idle. At this time, the system stops the app’s background services, just as if the app had called the services’ Service.stopSelf() methods.

Эпоха Service-ов тоже ушла, и они будут убиты операционной системой почти сразу после закрытия пользовательского интерфейса. Но Service может себя зарегистрировать как foreground, и если я его запущу сразу после загрузки устройства, это решит мою задачу (хоть и выглядит костылем).

Я же все еще могу создать BroadcastReceiver для получения BOOT_COMPLETED, потому что он в белом списке, правильно?

Новость 3. With Android 8.0… the system doesn’t allow a background app to create a background service.

И это значит, что BroadcastReceiver больше не может запускать Service.

Но, к счастью, в API 26 появился startForegroundService(). Он дает 5 секунд на то, чтобы Service вызвал startForeground() и показал пользователю нотификацию (иначе приложение будет убито).

Новость 4. Starting in Android 8.0 (API level 26), all notifications must be assigned to a channel.

Нотификация обязательна при создании foreground Service. Но в создании нотификаций тоже новшества, нужно определить “канал”. Каналы оказались совсем не страшными, в документации все неплохо описано и есть пример, который можно скопипастить.

Happy End

Квест решен.

Еще раз: в манифесте регистрируем BroadcastReceiver для BOOT_COMPLETED -> BroadcastReceiver запускает Service с помощью startForegroundService() -> Service регистрирует себя как foreground -> Service создает и регистрирует BroadcastReceiver-ы.

Если вам надо вызвать из BroadcastReceiver что-нибудь проще, чем долгоиграющий Service, то используйте Job-ы. Для совместимости со старыми версиями Гугл предложил JobIntentService, о его использовании неплохо написано в этой статье. И есть библиотека Android-Job от Evernote, предоставляющая универсальный интерфейс для запуска задач, под капотом использующая подходящий для конкретного API способ.

Код

В итоге, в коде это выглядит так:

class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, broadcast: Intent?) {
        Intent(context, ReceiverService::class.java).also { intent ->
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                context.startForegroundService(intent)
            } else {
                context.startService(intent)
            }
        }
    }
}

Регистрация foreground service:

class ReceiverService : Service() {
    ....
    
    val CHANNEL_ID = "fooChannel"

    // https://developer.android.com/training/notify-user/build-notification#Priority
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = getString(R.string.channel_name)

            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel(CHANNEL_ID, name, importance)

            val notificationManager: NotificationManager =
                    getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

            notificationManager.createNotificationChannel(channel)
        }
    }

    override fun onCreate() {
        super.onCreate()

        createNotificationChannel()

        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentText("Reboot watchdog")
                .setOngoing(true)
                .setAutoCancel(false)
                .build()

        startForeground(1, notification)

        .....
    }

}

И, собственно, работа, ради которой все затевалось:

class ReceiverService : Service() {
    private val pingReceiver by lazy { PingReceiver() }
    private val rebootReceiver by lazy { RebootReceiver() }
    
    ....

    override fun onCreate() {
        ....

        registerReceiver(pingReceiver, IntentFilter("com.agoda.PING"))
        registerReceiver(rebootReceiver, IntentFilter("com.agoda.REBOOT"))
    }

    override fun onDestroy() {
        super.onDestroy()

        unregisterReceiver(pingReceiver)
        unregisterReceiver(rebootReceiver)
    }
}

Не забываем в манифесте запросить необходимые разрешения:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<application>
    ....
    <receiver
        android:name=".BootReceiver"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
    </receiver>

    <service
        android:name=".ReceiverService"
        android:enabled="true"
        android:exported="true" />
 </application>