Как создать уведомление о звонке на Android: примеры кода

Разберёмся с уведомлениями о входящих звонках: начнём с самых простых и минималистичных, а закончим полноэкранными уведомлениями с внесистемным дизайном. Приступим!
Создание канала (API 26+)
Начиная с Android 8.0, каждому уведомлению должен быть назначен канал уведомлений, к которому оно относится. Раньше пользователь мог либо разрешить, либо запретить приложению показывать уведомления целиком — без возможности отключить только определённые типы. Это было неудобно. С появлением каналов пользователь может отключить назойливые уведомления (например, рекламу или лишние напоминания), оставив при этом важные — новые сообщения, звонки и так далее.
Если мы не укажем ID канала (используя устаревший билдер) или канал с таким ID не был создан, уведомление просто не отобразится на Android версии 8 и выше.
Нам понадобится библиотека androidx.core (скорее всего, она уже подключена). Мы пишем на Kotlin, поэтому используем её версию для этого языка:
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
}
Вся работа с уведомлениями осуществляется через системный сервис `NotificationManager`. Чтобы обеспечить обратную совместимость, при наличии стоит использовать версию с суффиксом `*Compat` — в нашем случае это `NotificationManagerCompat`. Чтобы получить экземпляр:
val notificationManager = NotificationManagerCompat.from(context)
Создаём наш канал. Каналу можно задать множество параметров — например, общий звук для уведомлений и паттерн вибрации. Мы настроим только самое необходимое, а полный список параметров смотрите здесь.
val INCOMING_CALL_CHANNEL_ID = “incoming_call”
// Создаём объект с данными канала
val channel = NotificationChannelCompat.Builder(
// channel ID, it must be unique within the package
INCOMING_CALL_CHANNEL_ID,
// "Важность"" уведомления, влияет на то, издаёт ли уведомление звук, показывается ли сразу и так далее. Ставим на максимум, звонок всё-таки
NotificationManagerCompat.IMPORTANCE_HIGH
)
// the name of the channel, which will be displayed in the system notification settings of the application
.setName(“Incoming calls”)
// Описание канала, будет отображаться там же
.setDescription(“Incoming audio and video call alerts”)
.build()
// Создаём канал. Если такой канал уже существует, то ничего не произойдёт, поэтому этот метод можно вызывать перед каждой отправкой уведомления в канал.
notificationManager.createNotificationChannel(channel)

Отображение уведомления
Чудесно, теперь мы можем приступить к созданию самого уведомления. Начнём с самого простого примера:
val notificationBuilder = NotificationCompat.Builder(
this,
// Снова ID канала
INCOMING_CALL_CHANNEL_ID
)
// Маленькая иконка, которая будет отображаться в статус баре
.setSmallIcon(R.drawable.icon)
// Название уведомления
.setContentTitle(“Incoming call”)
// Текст уведомления, обычно — имя звонящего
.setContentText(“James Smith”)
// Большое изображение, обычно - фотография / аватарка звонящего
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.logo))
// Для уведомления о входящем звонке разумно сделать так, чтобы его нельзя было "смахнуть"
.setOngoing(true)
Пока что мы создали своего рода «описание» уведомления, но оно ещё не показано пользователю. Чтобы его отобразить, снова обратимся к менеджеру:
// Let’s get to building our notification
val notification = notificationBuilder.build()
// We ask the system to display it
notificationManager.notify(INCOMING_CALL_NOTIFICATION_ID, notification)

`INCOMING_CALL_NOTIFICATION_ID` — это идентификатор уведомления, с помощью которого можно найти уже показанное уведомление и взаимодействовать с ним.
Например, пользователь долго не отвечал на звонок, звонящему надоело ждать, и он отменил вызов. Тогда мы можем отменить уведомление:
notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID)
Или, в случае приложения для конференций, если к вызывающему присоединилось несколько человек, мы можем обновить уведомление. Для этого достаточно создать новое уведомление и в вызове `notify` передать тот же id — тогда старое уведомление просто обновится новыми данными, без анимации появления нового. При этом можно переиспользовать старый `notificationBuilder`, просто заменив в нём изменившуюся часть:
notificationBuilder.setContentText(“James Smith, George Watson”)
notificationManager.notify(
INCOMING_CALL_NOTIFICATION_ID,
notificationBuilder.build()
)
Действия при нажатии на кнопку
Простое уведомление о входящем звонке, после которого пользователю самому нужно найти наше приложение и принять или отклонить звонок — не очень удобно. К счастью, мы можем добавить в уведомление кнопки действий!
Для этого при создании уведомления нужно добавить один или несколько `action`. Их создание выглядит примерно так:
val action = NotificationCompat.Action.Builder(
// Иконка, которая будет отображаться на кнопке (или не будет, зависит от версии Android)
IconCompat.createWithResource(applicationContext, R.drawable.icon_accept_call),
// Текст на кнопке
getString(R.string.accept_call),
// Само действие, PendingIntentt
acceptCallIntent
).build()
Погодите-ка, что ещё за `PendingIntent`? Это довольно широкая тема, достойная отдельной статьи, но в упрощённом виде — это способ указать, как запустить компонент приложения, например activity или service. В самом простом случае он создаётся так:
const val ACTION_ACCEPT_CALL = 101
// Создаём обычный intent, как при запуске новой Activity
val intent = Intent(applicationContext, MainActivity::class.java).apply { action = ACTION_ACCEPT_CALL}
// Но мы его не запускаем сами, а передаём в PendingIntent, который будет вызван потом, при нажатии на кнопку
val acceptCallIntent = PendingIntent.getActivity(applicationContext, REQUEST_CODE_ACCEPT_CALL, intent, PendingIntent.FLAG_UPDATE_CURRENT)
Соответственно, нам нужно обработать это действие в самом `acitivity`
Для этого в `onCreate()` (и в `onNewIntent()`, если вы используете флаг `FLAG_ACTIVITY_ SINGLE_TOP` для своего activity) получаем `action` из `intent` и выполняем соответствующее действие:
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent?.action == ACTION_ACCEPT_CALL) imaginaryCallManager.acceptCall()}
Теперь, когда всё готово, мы можем добавить действие к уведомлению через `Builder`
notificationBuilder.addAction(action)

Помимо кнопок, мы можем назначить действие по нажатию на само уведомление — вне кнопок. Переход на экран входящего звонка кажется самым удачным решением — для этого повторяем все шаги по созданию action, но вместо `ACTION_ACCEPT_CALL` используем другой id действия, а в `MainActivity.onCreate()` обрабатываем это `action` навигацией
override fun onNewIntent(intent: Intent?) {
…
if (intent?.action == ACTION_SHOW_INCOMING_CALL_SCREEN)
imaginaryNavigator.navigate(IncomingCallScreen())
}
Для обработки событий также можно использовать `service` вместо `activity`.
Уведомления с собственным дизайном
Уведомления — это часть системного интерфейса, поэтому отображаются они в едином, системном стиле. Однако, если хочется выделиться или стандартное расположение кнопок и других элементов не устраивает, можно задать уведомлениям свой уникальный стиль.
DISCLAIMER: из-за огромного количества различных устройств Android с разными размерами и соотношениями сторон экранов, а также ограниченных возможностей позиционирования элементов в уведомлениях (в отличие от обычных экранов приложений), Custom Content Notification значительно сложнее в поддержке
Отрисовываться уведомление будет по-прежнему системой, то есть вне процесса нашего приложения, поэтому вместо обычного View нам нужно использовать RemoteViews. Обратите внимание, что этот механизм поддерживает не все привычные элементы — особенно сильно сказывается отсутствие `ConstraintLayout`.
Простой пример — custom notification с одной кнопкой «accept call».
RelativeLayout
…
android:layout_width=”match_parent”
android:layout_height=”match_parent”>
Button
android:id=”@+id/button_accept_call”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_centerHorizontal=”true”
android:layout_alignParentBottom=”true”
android:backgroundTint=”@color/green_accept”
android:text=”@string/accept_call”
android:textColor=”@color/fora_white”
RelativeLayout
Разметка готова, теперь нужно создать экземпляр `RemoteViews` и передать его в конструктор уведомления
val remoteView = RemoteViews(packageName, R.layout.notification_custom)
// Устанавливаем PendingIntent, который будет "выстреливать" при нажатии на кнопку. Обычный onClickListener тут не подойдёт -- опять же, уведомление будет жить вне нашего процесса
remoteView.setOnClickPendingIntent(R.id.button_accept_call, pendingIntent)
// Добавляем в наш многострадальный builder
notificationBuilder.setCustomContentView(remoteView)

Наш пример максимально упрощён и, конечно, выглядит не очень естественно. Обычно кастомизированное уведомление делается в стиле, близком к системному, но с использованием брендовой цветовой гаммы — как, например, в Skype.
Помимо `.setCustomContentView` (обычное уведомление), мы можем отдельно задать разметку для расширенного состояния (`.setCustomBigContentView`) и для режима «на экране» (`.setCustomHeadsUpContentView`)
Полноэкранные уведомления
Теперь наши уведомления выглядят так же, как и в приложении, но всё ещё остаются небольшими с маленькими кнопками. А что происходит, когда на телефон поступает обычный входящий вызов? Перед глазами появляется красивый полноэкранный экран. К счастью, такой функционал доступен и нам! При этом мы не сталкиваемся с ограничениями `RemoteViews` — можем показывать полноценную `activity`
В первую очередь добавьте разрешение в `AndroidManifest.xml`
uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"
Создав `activity` с нужным дизайном и функциональностью, инициализируем `PendingIntent` и добавляем его к уведомлению:
val intent = Intent(this, FullscreenNotificationActivity::class.java)
val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
// Заодно устанавливаем highPriority в true, так что highPriority, если не входящий звонок?
notificationBuilder.setFullScreenIntent(pendingIntent, highPriority = true)
Да, и всё! Несмотря на то, что эту функциональность так легко добавить, по какой-то причине далеко не все приложения для звонков ею пользуются. А ведь у гигантов вроде WhatsApp и Telegram уведомления о входящих звонках реализованы именно так!

Итог
Уведомление о входящем звонке — очень важная часть приложения. К нему много требований: оно должно быть оперативным, привлекать внимание, но при этом не раздражать. Сегодня мы рассказали о доступных инструментах, которые помогут достичь всех этих целей. Пусть ваши уведомления всегда будут удобными и эффективными!
