You will learn how to make incoming call notifications on Android from basic to advanced layouts from this article. Customize the notification screen with our examples.

Last time, we told you what any Android app with calls should have and promised to show you how to implement it. Today we’ll deal with notifications for incoming calls: we’ll start with the simplest and most minimalistic ones, and end with full-screen notifications with an off-system design. Let’s get started!

Creating a channel(api 26+)

Since Android 8.0, each notification must have a notification channel to which it belongs. Before this version of the system, the user could either allow or disallow the app to show notifications, without being able to turn off only a certain category, which was not very convenient. With channels, on the other hand, the user can turn off annoying notifications from the app, such as ads and unnecessary reminders, while leaving only the ones he needs (new messages, calls, and so on).

If we don’t specify a channel ID, using the Deprecated builder. If we don’t create a channel with such an ID, the notification will not be displayed with the Android 8 or later versions.

We need the androidx.core library which you probably already have hooked up. We write in Kotlin, so we use the version of the library for that language:

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

All work with notifications is done through the system service NotificationManager. For backward compatibility, it is always better to use the Compat version of Android classes if you have them, so we will use NotificationManagerCompat. To get the instance:

 
val notificationManager = NotificationManagerCompat.from(context) 
	

Let’s create our channel. You can set a lot of parameters for the Let’s create our channel. You can set a lot of parameters for the channel, such as a general sound for notifications and a vibration pattern. We will set only the basic ones, and the full list you can find here.

 
val INCOMING_CALL_CHANNEL_ID = “incoming_call”  
// Creating an object with channel data  
val channel = NotificationChannelCompat.Builder(  
   // channel ID, it must be unique within the package  
   INCOMING_CALL_CHANNEL_ID,  
    // The importance of the notification affects whether the notification makes a sound, is shown immediately, and so on. We set it to maximum, it’s a call after all.  
   NotificationManagerCompat.IMPORTANCE_HIGH  
)  
   // the name of the channel, which will be displayed in the system notification settings of the application  
   .setName(“Incoming calls”)  
   // channel description, will be displayed in the same place  
   .setDescription(“Incoming audio and video call alerts”)  
   .build()  
// Creating the channel. If such a channel already exists, nothing happens, so this method can be used before sending each notification to the channel.  
notificationManager.createNotificationChannel(channel) 
	
Как создать канал для уведомления на Android
Creating a channel of a call notification on Android

Notification runtime permission (api 33+):

If your app targets Android 13+ you should declare the following permission in AndroidManifest:

 
 uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> 
 

You should also request POST_NOTIFICATIONS permission from a user at runtime. Learn more about permission requesting.

Displaying a notification

Now we can start creating the notification itself, let’s start with the simplest example:

 
val notificationBuilder = NotificationCompat.Builder(   
this,   
   // channel ID again  
   INCOMING_CALL_CHANNEL_ID  
)  
   // A small icon that will be displayed in the status bar   
   .setSmallIcon(R.drawable.icon)  
   // Notification title  
   .setContentTitle(“Incoming call”)  
   // Notification text, usually the caller’s name 
   .setContentText(“James Smith”)  
   // Large image, usually a photo / avatar of the caller  
   .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.logo))  
   // For notification of an incoming call, it’s wise to make it so that it can’t be “swiped” 
   .setOngoing(true)
          So far we’ve only created a sort of “description” of the notification, but it’s not yet shown to the user. To display it, let’s turn to the manager again:  
// 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
Simple notification

The INCOMING_CALL_NOTIFICATION_ID is a notification identifier that can be used to find and interact with an already displayed notification.

For example, the user wasn’t answering the call for a long time, the caller got tired of waiting and canceled the call. Then we can cancel notification:

 
notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID)
 

Or, in the case of a conferencing application, if more than one person has joined the caller, we can update our notification. To do this, just create a new notification and pass the same notification ID in the notify call—then the old notification will just be updated with the data, without animating the appearance of the new notification. To do this, we can reuse the old notificationBuilder by simply replacing the changed part in it:

 
notificationBuilder.setContentText(“James Smith, George Watson”)  
notificationManager.notify(  
  INCOMING_CALL_NOTIFICATION_ID,   
   notificationBuilder.build()  
) 
 

Button actions upon clicking

A simple notification of an incoming call, after which the user has to find our application himself and accept or reject the call is not a very useful thing. Fortunately, we can add action buttons to our notification!

To do this, we add one or more actions when creating the notification. Creating them will look something like this:

 
val action = NotificationCompat.Action.Builder(  
   // The icon that will be displayed on the button (or not, depends on the Android version)   
   IconCompat.createWithResource(applicationContext, R.drawable.icon_accept_call),  
   // The text on the button  
   getString(R.string.accept_call),  
   // The action itself, PendingIntent 
   acceptCallIntent  
).build() 
 

Wait a minute, what does another PendingIntent mean? It’s a very broad topic, worthy of its own article, but simplistically, it’s a description of how to run an element of our application (such as an activity). In its simplest form it goes like this:

 
 const val ACTION_ACCEPT_CALL = 101
// We create a normal intent, just like when we start a new Activity 
val intent = Intent(applicationContext, MainActivity::class.java).apply { action = ACTION_ACCEPT_CALL}
// But we don’t run it ourselves, we pass it to PendingIntent, which will be called later when the button is pressed
val acceptCallIntent = PendingIntent.getActivity(applicationContext, REQUEST_CODE_ACCEPT_CALL, intent, PendingIntent.FLAG_UPDATE_CURRENT)
 

Accordingly, we need to handle this action in activity itself

To do this, in `onCreate()` (and in `onNewIntent()` if you use the flag `FLAG_ACTIVITY_SINGLE_TOP` for your activity), take `action` from `intent` and take the action:

 
 override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent?.action == ACTION_ACCEPT_CALL) imaginaryCallManager.acceptCall()}
 

Now that we have everything ready for our action, we can add it to our notification via `Builder`:

 
notificationBuilder.addAction(action)
 
Как добавить действие к уведомлению на Android
Notification with action buttons

In addition to the buttons, we can assign an action by clicking on the notification itself, outside of the buttons. Going to the incoming call screen seems like the best solution — to do this, we repeat all the steps of creating an action, but use a different action id instead of `ACTION_ACCEPT_CALL`, and in `MainActivity.onCreate()` handle that `action` with navigation:

 
 override fun onNewIntent(intent: Intent?) {  
   …  
   if (intent?.action == ACTION_SHOW_INCOMING_CALL_SCREEN)  
       imaginaryNavigator.navigate(IncomingCallScreen())  
} 
 

Notification.CallStyle (api 31+):

There’s a new call notification style in Android 12, Notification.CallStyle. It helps you distinguish and highlight the call notification among others. In the new OS versions starting from Android 12 you have to use Notification.CallStyle instead of custom call notifications.

 
 // Dividing the logic of creating notifications for Android 12+ below 
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 
   // Creating a notification with Notification.CallStyle 
   val icon = Icon.createWithResource(this, R.drawable.user_avatar) 
   val caller = Person.Builder() 
       // Caller icon 
       .setIcon(icon) 
       // Caller name 
       .setName("Chuck Norris") 
       .setImportant(true) 
       .build() 
   // Creating the call notification style 
   val notificationStyle = Notification.CallStyle.forIncomingCall(caller, declineIntent, answerIntent) 
   Notification.Builder(this, CHANNEL_ID) 
       .setSmallIcon(R.drawable.ic_launcher_foreground) 
       .setContentTitle("Incoming call") 
       .setContentText("Incoming call from Chuck Norris") 
       .setStyle(notificationStyle) 
       // Intent that will be called for when tapping on the notification 
       .setContentIntent(contentIntent) 
       .setFullScreenIntent(contentIntent, true) 
       .setOngoing(true) 
       // notification category that describes this Notification. May be used by the system for ranking and filtering 
       .setCategory(Notification.CATEGORY_CALL) 
       .build() 
} else { 
   // Creating the custom notification 
   ... 
} 
 
Notification with Notification.CallStyle
Notification with Notification.CallStyle

Notifications with their own design

Notifications themselves are part of the system interface, so they will be displayed in the same system style. However, if you want to stand out, or if the standard arrangement of buttons and other notification elements don’t suit you, you can give the notifications your own unique style.

DISCLAIMER: Due to the huge variety of Android devices with different screen sizes and aspect ratios, combined with the limited positioning of elements in notifications (relative to regular application screens), Custom Content Notification is much more difficult to support

The notification will still be rendered by the system, that is, outside of our application process, so we need to use RemoteViews instead of the regular View. Note that this mechanism does not support all the familiar elements, in particular, the `ConstraintLayout` is not available.

A simple example is a custom notification with one button for accepting a 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
 

The layout is ready, now we need to create an instance RemoteViews and pass it to the notification constructor.

 
 val remoteView = RemoteViews(packageName, R.layout.notification_custom)
// Set the PendingIntent that will “shoot” when the button is clicked. A normal onClickListener won’t work here – again, the notification will live outside our process
remoteView.setOnClickPendingIntent(R.id.button_accept_call, pendingIntent)
// Add to our long-suffering builder  
notificationBuilder.setCustomContentView(remoteView)
 
Как настроить собственный дизайн для Android уведомления
Notification with custom layout

Our example is as simplistic as possible and, of course, a bit jarring. Usually, a customized notification is done in a style similar to the system notification, but in a branded color scheme, like the notifications in Skype, for example.

In addition to .setCustomContentView, which is a normal notification, we can separately specify mark-up for the expanded state .setCustomBigContentView and for the head-up state .setCustomHeadsUpContentView

Full-screen notifications

Now our custom notification layouts match the design inside the app, but they’re still small notifications, with small buttons. And what happens when you get a normal incoming call? Our eyes are presented with a beautiful screen that takes up all the available space. Fortunately, this functionality is available to us! And we’re not afraid of any limitations associated with RemoteViews, as we can show the full `activity`.

First of all, we have to add a permission to `AndroidManifest.xml`

 
 uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"
 

After creating an `activity` with the desired design and functionality, we initialize the PendingIntent and add it to the notification:

 
val intent = Intent(this, FullscreenNotificationActivity::class.java)
val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
// At the same time we set highPriority to true, so what is highPriority if not an incoming call?  
notificationBuilder.setFullScreenIntent(pendingIntent, highPriority = true)
 

And that’s it! Despite the fact that this functionality is so easy to add, for some reason not all call-related applications use it. However, giants like Whatsapp and Telegram have implemented notifications of incoming calls in this way!

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

Bottom line

The incoming call notification on Android is a very important part of the application. There are a lot of requirements: it should be prompt, eye-catching, but not annoying. Today we learned about the tools available to achieve all these goals. Let your notifications be always beautiful!

  • Development