Разберёмся с уведомлениями о входящих звонках: начнём с самых простых и минималистичных, а закончим полноэкранными уведомлениями с внесистемным дизайном. Приступим!

Создание канала (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) 
	
Как создать канал для уведомления на Android
Как создать канал для уведомлений на Android

Отображение уведомления

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

 
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) 
 
Как настроить отображение уведомления на Android
Как настроить отображение уведомления на Android

`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)
 
Как добавить действие к уведомлению на Android
Как добавить действие к уведомлению на Android

Помимо кнопок, мы можем назначить действие по нажатию на само уведомление — вне кнопок. Переход на экран входящего звонка кажется самым удачным решением — для этого повторяем все шаги по созданию 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)
 
Как настроить собственный дизайн для Android уведомления
Как настроить собственный дизайн для Android-уведомления

Наш пример максимально упрощён и, конечно, выглядит не очень естественно. Обычно кастомизированное уведомление делается в стиле, близком к системному, но с использованием брендовой цветовой гаммы — как, например, в 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 уведомления о входящих звонках реализованы именно так!

Как сделать уведомление Android на весь экран?
Как сделать уведомление Android на весь экран?

Итог

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

  • Разработка