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

Создание канала (api 26+)

Начиная с Android 8.0, у каждого уведомления должен быть указан канал уведомлений, которому оно принадлежит. До этой версии системы пользователь мог либо разрешить, либо запретить приложению показывать уведомления, не имея возможности выключить лишь определённую категорию, что было не очень удобно. С каналами же пользователь может выключить назойливые уведомления от приложения (реклама, лишние напоминания), оставив при этом только нужные ему (новые сообщения, звонки и так далее).

Если мы не укажем ID канала (воспользовавшись Deprecated билдером) или канал с таким ID не был нами создан, то уведомление попросту не отобразится на версиях Android выше 8.

Нам потребуется библиотека androidx.core (скорее всего, она уже у вас подключена). Мы пишем на Kotlin, поэтому используем версию библиотеки для этого языка:

 
dependencies {  
   implementation("androidx.core:core-ktx:1.9.0") 
}
	

Вся работа с уведомлениями происходит через системный сервис `NotificationManager`. Для обратной совместимости всегда лучше использовать `*Compat` версию android-классов при их наличии, поэтому мы воспользуемся `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` (обычное уведомление) мы можем отдельно указать разметку для expanded состояния (`.setCustomBigContentView`) и для head-up состояния (`.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 на весь экран?

Итог

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

  • Разработка