Categories
Uncategorized

How to Implement Foreground Service and Deep Links for Android apps with calls? With Code Examples

Let’s take a look at 2 more UX conveniences for the Android caller application. First, let’s make sure that the app continues to function normally after minimizing or locking the screen with Android Foreground Services. After that, let’s see how we can implement direct links to a call or conference with Deep Links. By clicking on them, the smartphone users will be taken directly to the call.

How to create a Foreground Service on Android

Today’s smartphones and their operating systems have many built-in optimizations aimed at extending battery life. And mobile app developers need to keep in mind the potential actions the system can take on the app. 

A prime example is freeing up resources and closing apps that the user is not actively interacting with at the moment. In this case, the system considers only the app that is currently displayed on the user’s screen to be “actively used”. All other running applications can be closed at any time if the system does not have enough resources for the actively used one. Thanks to this, we can open an infinite number of applications and not explicitly close them — the system will close the old ones, and when we return to them, the application will run again.

In general, this mechanism is convenient and necessary on mobile devices. But we want to bypass this restriction so that the call is protected from sudden closure by the system. Fortunately, it is possible to “mark” a part of the application as actively used, even if it is not displayed anymore. To do this, we use the Foreground Service. Note that even this does not give full protection from the system — but it increases the “priority” of the application in the eyes of the system and also allows you to keep some objects in memory even if `Activity` is closed.

Add permission to run such Android services:

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

Let’s implement our service itself. In its simplest form it’s just a subclass Service, which has a link to our `CallManager` (so it won’t be cleaned up by garbage collector):

class OngoingCallService : Service() {

    @Inject
    lateinit var abstractCallManager: AbstractCallManager

    // Implementation of an abstract method; we won’t use Bind so just return null
    override fun onBind(intent: Intent): IBinder? = null

}

Service is an application component and, like Activity, must be specified in `AndroidManifest.xml`:

<service
    // Class name of our service
    android:name=".OngoingCallService"
    android:enabled="true"
    // This flag meant that other applications can’t run this service
    android:exported="false"
    // Declare a type of our service
    android:foregroundServiceType="microphone|camera|phoneCall" />

Our Android Foreground Service starts up a bit differently than regular services:

private fun startForegroundService() {
    val intent = Intent(this, OngoingCallService::class.java)
    ContextCompat.startForegroundService(this, intent)
}

On Android versions above 8, the Foreground Service must call the startForeground method within a few seconds, otherwise, the application is considered to be hung (ANR). It is necessary to pass a notification to this method because, for security reasons, the presence of such services should be visible to the user (if you do not know or have forgotten how to create notifications, you can refresh your memory in one of our previous articles about call notifications on Android):

val notification = getNotification()
startForeground(ONGOING_NOTIFICATION_ID, notification)

Everything that we wrote in the previous article about notifications applies to this notification — you can update it with the list of call participants, add buttons to it, or change its design completely. The only difference is that this notification will be `ongoing` by default and users won’t be able to “swipe” it.

When the call is over – the service must be stopped, otherwise, the application can be completely closed only through the settings, which is very inconvenient for users. Our service is stopped in the same way as usual Android services:

private fun stopForegroundService() {
    val intent = Intent(this, OngoingCallService::class.java)
    stopService(intent)
}

Starting and stopping a service is very convenient to implement if CallManager has a reactive field to monitor the status of the call, for example:

abstractCallManager.isInCall
    .collect { if (it) startForegroundService() else stopForegroundService() }

This is the whole implementation of the service, which will allow to some extent protect our minimized application from being closed by the system.

Android Deep Links Tutorial

An extremely user-friendly feature that simplifies the growth of the user base of the app is the links to a certain place in the app. If the user doesn’t have the app, the link opens a page on Google Play. In the context of call apps, the most successful use case is the ability to share a link to a call / meeting / room. The user wants to talk to someone, throws the link to the person he’s talking to, that person downloads the app, and then gets right into the call — what could be more convenient?

The links themselves to a particular location in the application are supported by the system without any additional libraries. But in order for the link to “survive” the installation of the application, we need to ask for help from Firebase Dynamic Links.

Let’s concentrate on the implementation of links handling in the application and leave their creation to backend developers.

So, the Android deep links with code examples. First, let’s add the library:

dependencies {
    implementation 'com.google.firebase:firebase-dynamic-links:20.1.1'
}

To the user, deep links are ordinary links that he clicks on. But before opening a link in the browser, the system looks through the registry of applications and finds those that have declared that they handle links of this domain. If such an application is found – instead of opening in the browser, it launches the same application and the link is passed to it. If there is more than one such application – the system window will be shown with a list where the user can choose which application to open the link with. If you own the link domain, you can protect yourself from opening such links by other applications while yours is installed.

To declare the links that our app can handle, we need to add our `Activity` an intent-filter in `AndroidManifest.xml`:

<activity ...>
    <intent-filter>
        // These action and category notify the system that we can “display” the links
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        // Description of the link which we can handle. In this case these are the links starting from calls://forasoft.com
        <data
            android:host="forasoft.com"
            android:scheme="calls"/>
    </intent-filter>
</activity>

When the user clicks the Dynamic Link and installs the application (or clicks on the link having the app already installed), the Activity will launch which is indicated as this link’s handler. In this Activity, we can get the link this way:

Firebase.dynamicLinks
        .getDynamicLink(intent)
        .addOnSuccessListener(this) { data ->
            val deepLink: Uri? = data?.link
        }

When using regular deep links, the data becomes a bit simpler:

val deepLink = intent?.data

That’s all, now all we have left is getting the parameters that interest us from the link and carrying out the actions in your application that are necessary to connect to the call:

    val meetindId = deepLink?.getQueryParameter("meetingid")
        if (meetingId != null) abstractCallManager.joinMeeting(meetingId)

Conclusion

In the final article of our cycle “what each application with calls should have” we’ve gone through keeping our application alive after minimizing it and using the deep links as a convenient option for invitations to the call. Now you know all the mechanisms that make the user experience better not only inside the application but also at the system level.

Read other articles from this cycle:

What Every Android App With Calls Should Have

How to Make a Custom Call Notification on Android? With Code Examples

How to Make Picture-in-Picture Mode on Android With Code Examples

How to Implement Audio Output Switching During the Call on Android App?

Reach out to us to develop your app with calls, either for Android or other platforms 🙂

Categories
Uncategorized

How to Implement Audio Output Switching During the Call on Android App?

android-audio-output-tutorial
Automatically change your audio output on Android app

Seamless and timely switching between the sound output devices on Android is a feature that is usually taken for granted, but the lack of it (or problems with it) is very annoying. Today we will analyze how to implement such switching in Android ringtones, starting from the manual switching by the user to the automatic switching when headsets are connected. At the same time, let’s talk about pausing the rest of the audio system for the duration of the call. This implementation is suitable for almost all calling applications since it operates at the system level rather than the call engine level, e.g., WebRTC.

Audio output device management

    All management of Android sound output devices is implemented through the system’s `AudioManager`. To work with it you need to add permission to `AndroidManifest.xml`:

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

    First of all, when a call starts in our app, it is highly recommended to capture the audio focus — let the system know that the user is now communicating with someone, and it is best not to be distracted by sounds from other apps. For example, if the user was listening to music, but received a call and answered — the music will be paused for the duration of the call.

    There are two mechanisms of audio focus request — the old one is deprecated, and the new one is available since Android 8.0. We implement for all versions of the system:

// Receiving an AudioManager sample
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
// We need a "request" for the new approach. Let's generate it for versions >=8.0 and leave null for older ones
@RequiresApi(Build.VERSION_CODES.O)
private fun getAudioFocusRequest() =
   AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).build()


// Focus request
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    // Use the generated request
    audioManager.requestAudioFocus(getAudioFocusRequest())
} else {
    audioManager.requestAudioFocus(
        // Listener of receiving focus. Let's leave it empty for the sake of simpleness
        { },
        // Requesting a call focus
        AudioAttributes.CONTENT_TYPE_SPEECH,
        AudioManager.AUDIOFOCUS_GAIN
    )
}

    It is important to specify the most appropriate `ContentType` and `Usage` — based on these, the system determines which of the custom volume settings to use (media volume or ringer volume) and what to do with the other audio sources (mute, pause, or allow to run as before).

val savedAudioMode = audioManager.mode
val savedIsSpeakerOn = audioManager.isSpeakerphoneOn
val savedIsMicrophoneMuted = audioManager.isMicrophoneMute

        Great, we’ve got audio focus. It is highly recommended to save the original AudioManager settings right away before changing anything – this will allow us to restore it to its previous state when the call is over. You should agree that it would be very inconvenient if one application’s volume control would affect all the others

        Now we can start setting our defaults. It may depend on the type of call (usually audio calls are on “speakerphone” and video calls are on “speakerphone”), on the user settings in the application or just on the last used speakerphone. Our conditional app is a video app, so we’ll set up the speakerphone right away:

// Moving AudioManager to the "call" state
audioManager.mode = AudioSystem.MODE_IN_COMMUNICATION
// Enabling speakerphone
audioManager.isSpeakerphoneOn = true

 Great, we have applied the default settings. If the application design provides a button to toggle the speakerphone, we can now very easily implement its handling:

audioManager.isSpeakerphoneOn = !audioManager.isSpeakerphoneOn

Monitoring the connection of headphones

        We’ve learned how to implement hands-free switching, but what happens if you connect headphones? Nothing, because `audioManager.isSpeakerphoneOn` is still `true`! And the user, of course, expects that when headphones are plugged in, the sound will start playing through them. And vice versa — if we have a video call, then when we disconnect the headphones the sound should start playing through the speakerphone. 

        There is no way out, we have to monitor the connection of the headphones. Let me tell you right away, the connection of wired and Bluetooth headphones is tracked differently, so we have to implement two mechanisms at once. Let’s start with wired ones and put the logic in a separate class:

class HeadsetStateProvider(
    private val context: Context,
    private val audioManager: AudioManager
) {
    // The current state of wired headies; true means enabled
    val isHeadsetPlugged = MutableStateFlow(getHeadsetState())

    // Create BroadcastReceiver to track the headset connection and disconnection events
    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent) {
            if (intent.action == AudioManager.ACTION_HEADSET_PLUG) {
                when (intent.getIntExtra("state", -1)) {
                    // 0 -- the headset is offline, 1 -- the headset is online
                    0 -> isHeadsetPlugged.value = false
                    1 -> isHeadsetPlugged.value = true
                }
            }
        }
    }

    init {
        val filter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
        // Register our BroadcastReceiver
        context.registerReceiver(receiver, filter)
    }

    // The method to receive a current headset state. It's used to initialize the starting point.
    fun getHeadsetState(): Boolean {
        val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
        return audioDevices.any {
            it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
                    || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET
        }
    }
}

   In our example, we use `StateFlow` to implement subscription to the connection state, but instead, we can implement, for example, `HeadsetStateProviderListener`

        Now just initialize this class and observe the `isHeadsetPlugged` field, turning the speaker on or off when it changes:

headsetStateProvider.isHeadsetPlugged
    // If the headset isn't on, speakerphone is.
    .onEach { audioManager.isSpeakerphoneOn = !it }
    .launchIn(someCoroutineScope)

Bluetooth headphones connection monitoring

            Now we implement the same monitoring mechanism for such Android sound output devices as Bluetooth headphones:

class BluetoothHeadsetStateProvider(
    private val context: Context,
 private val bluetoothManager: BluetoothManager
) {

    val isHeadsetConnected = MutableStateFlow(getHeadsetState())

    init {
        // Receive the adapter from BluetoothManager and install our ServiceListener
        bluetoothManager.adapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
            // This method will be used when the new device connects
            override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
                // Checking if it is the headset that's active
                if (profile == BluetoothProfile.HEADSET)
                    // Refreshing state
                    isHeadsetConnected.value = true
            }

            // This method will be used when the new device disconnects
            override fun onServiceDisconnected(profile: Int) 
                if (profile == BluetoothProfile.HEADSET)
                    isHeadsetConnected.value = false
            }
        // Enabling ServiceListener for headsets
        }, BluetoothProfile.HEADSET)
    }

    // The method of receiving the current state of the bluetooth headset. Only used to initialize the starting state
    private fun getHeadsetState(): Boolean {
        val adapter = bluetoothManager.adapter
        // Checking if there are active headsets  
        return adapter?.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothProfile.STATE_CONNECTED
    }

}

To work with Bluetooth, we need another resolution:

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

    And now to automatically turn on the speakerphone when no headset is connected, and vice versa when a new headset is connected:

combine(headsetStateProvider.isHeadsetPlugged, bluetoothHeadsetStateProvider.isHeadsetPlugged) { connected, bluetoothConnected ->
    audioManager.isSpeakerphoneOn = !connected && !bluetoothConnected
}
    .launchIn(someCoroutineScope)

Tidying up after ourselves.

When the call is over, the audio focus is no longer useful to us and we have to get rid of it. Let’s restore the settings we saved at the beginning:

audioManager.mode = savedAudioMode
audioManager.isMicrophoneMute = savedIsMicrophoneMuted
audioManager.isSpeakerphoneOn = savedIsSpeakerOn

And now, actually, let’s give away the focus. Again, the implementation depends on the system version:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    audioManager.abandonAudioFocusRequest(getAudioFocusRequest())
} else {
    // Listener для простоты опять оставим пустым
    audioManager.abandonAudioFocus { }
}

Other articles about Android calls

How to Make Picture-in-Picture Mode on Android With Code Examples

WebRTC on Android

How to Make a Custom Call Notification on Android? With Code Examples

What Every Android App With Calls Should Have

Bottom line

        Great, here we have implemented the perfect UX of switching between Android sound output devices in our app. The main advantage of this approach is that it is almost independent of the specific implementation of calls: in any case, the played audio will be controlled by `AudioManager’, and we control exactly at its level!

Categories
Uncategorized

How to Make Picture-in-Picture Mode on Android With Code Examples

This is how Picture-in-Picture mode looks like

In recent years, smartphones have become increasingly close to computers in terms of functionality, and many are already replacing the PC as their primary tool for work. The advantage of personal computers was multi-window capability, which remained unavailable on smartphones. But with the release of Android 7.0, this began to change and multi-window support appeared.

   It’s hard to overestimate the convenience of a small floating window with the video of the interlocutor when the call is minimized – you can continue the dialogue and simultaneously take notes or clarify some information. Android has two options for implementing this functionality: support for the application in a floating window and a picture-in-picture mode. Ideally, an application should support both approaches, but the floating window is more difficult to develop and imposes certain restrictions on the overall application design, so let’s consider picture-in-picture (PiP) on Android as a relatively simple way to bring multi-window support into your application.

pop up video call
PIP mode for video calls on Android

Switching to PIP mode

        Picture-in-picture mode is supported on most devices with Android 8 and above. Accordingly, if you support system versions lower than this, all PIP mode-related calls should be wrapped in the system version check:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 
    // Something related to PiP 
}

   The entire `Activity` is converted to PIP, and first, you need to declare PIP support for this `Activity` in `AndroidManifest.xml`:

<activity
    ...
    android:supportsPictureInPicture="true" />

       Before using picture-in-picture it is necessary to make sure that the user’s device supports this mode, to do this we turn to the `PackageManager`.

val isPipSupported = context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)

After that, in its simplest form, the transition to picture-in-picture mode is done literally with one line:

this.enterPictureInPictureMode()

   But to go to it, you need to know when it is convenient for the user. You can make a separate button and jump when you click on it. The most common approach is an automatic switch when the user minimizes the application during a call. To track this event, there is a handy method `Activity.onUserLeaveHint` called whenever the user intentionally leaves `Activity` — whether via the Home or Recent button.

override fun onUserLeaveHint() {
    ...
    if (isPipSupported && imaginaryCallManager.isInCall)
        this.enterPictureInPictureMode()
}

Interface adaptation

        Great, now our call screen automatically goes into PIP mode on Android! But there are often “end call” or “change camera” buttons, and they will not work in this mode. It’s better to hide them when transitioning.

        To track the transition to / from PIP mode, `Activity` and `Fragment` have a method `onPictureInPictureModeChanged`. Let’s redefine it and hide unnecessary interface elements

override fun onPictureInPictureModeChanged(
    isInPictureInPictureMode: Boolean,
    newConfig: Configuration?
) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
    setIsUiVisible(isInPictureInPictureMode)
}

   The PIP window is quite small, so it makes sense to hide everything except the interlocutor’s video, including the local user’s video — it will be too small to see anything there anyway.

How to implement picture-in-picture mode on Android app?

Customization

        The PIP window can be further customized by passing `PictureInPictureParams` in a call to `enterPictureInPictureMode`. There are not many customization options, but the option to add buttons to the bottom of the window deserves special attention. This is a nice way to keep the screen interactive despite the fact that the regular buttons stop working in PIP mode.

        The maximum number of buttons you can add depends on many factors, but you can always add at least three. All buttons over the limit simply won’t be shown, so it’s better to place the especially important ones at the beginning. You can find out the exact limit in the current configuration through the method `Activity`:

this.maxNumPictureInPictureActions

        Let’s add an end call button to our PIP window. To start with, just like with notifications, we need a `PendingIntent`, which will be responsible for telling our application that the button has been pressed. If this is the first time you’ve heard of `PendingIntent’ — you can learn more about them in our last article.

        After that, we can start creating the actual button description, namely `RemoteAction`.

val endCallPendingIntent = getPendingIntent()
val endCallAction = RemoteAction(
    // An icon for a button. The color will be ignored and changed to a system color
    Icon.createWithResource(this, R.drawable.ic_baseline_call_end_24),
    // Text of the button that won't be shown
    "End call",
    // ContentDescription для screen readers
    "End call button",
    // Our PendingIntent  that'll be launched upon pressing the button
    endCallPendingIntent
)

        Our “action” is ready, now we need to add it to the PIP parameters and, subsequently, to the mode transition call.

        Let’s start by creating a Builder for our customization parameters:

val pipParams = PictureInPictureParams.Builder()
    .setActions(listOf(endCallAction))
    .build()

this.enterPictureInPictureMode(pipParams)
multi-window mode
How to customize picture-in-picture mode?

In addition to the buttons, through the parameters, you can set the aspect ratio of the PIP features on Android or the animation of switching to this mode.

Other articles about calls on Android

WebRTC on Android

How to Make a Custom Call Notification on Android? With Code Examples

What Every Android App With Calls Should Have

How to Implement Audio Output Switching During the Call on Android App?

How to Implement Foreground Service and Deep Links for Android apps with calls? With Code Examples

    Conclusion

        We have considered a fairly simple but very handy variant of using the multi-window feature to improve the user experience, learned how to add buttons to the PIP window on Android, and adapt our interface when switching to and from this mode.

Categories
Uncategorized

How to Make a Custom Call Notification on Android? With Code Examples

How to create a custom Android call notification

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! 

Channel creation (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.5.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 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)
create-channel-notifcation-on-android
How to create notification channel on Android

Displaying a notification

Wonderful, 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)
set-up-display-android-notification
How to display a notification for Android

    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 or service). 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)
add-buttons-to-android-notification
How to add notification buttons on Android

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())

}

You can also use `service` instead of `activity` to handle events.

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:

<!– notification_custom.xml –>

<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)
create-custom-android-notification
How to create a custom notification on Android

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 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)

Yes, 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!

create-full-screen-android-notification
How to create a full screen notification on Android

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!

Categories
Uncategorized

What Every Android App With Calls Should Have

In today’s world, mobile communication is everything. We are surrounded by apps for audio and video calls, meetings, and broadcasts. With the pandemic, it’s not just business meetings that have moved from meeting rooms to calling apps. Calls to family, concerts, and even consultations with doctors are all now available on apps.

In this article we’ll cover the features every communication app should have, whether it’s a small program for calls or a platform for business meetings and webinars, and in the following articles, we’ll show you some examples of how to implement them.

Incoming call notification

Apps can send notifications to notify you of something important. There’s nothing more important for a communication app than an incoming call or a scheduled conference that the user forgot about.

So any app with call functionality has to use this mechanism to notify. Of course, we can show the name and the photo of the caller. Also, for the user’s convenience, we can add buttons to answer or reject the call without unnecessary clicks and opening the app.

You can go even further and change the notification design provided by the system.

However, options for Android devices don’t end here. Show a full-screen notification with your design even if the screen is locked! Read the guide on how to make your Android call notification here.

A notification that does not allow to close the process

The call may take a long time, so the user decides to do something at the same time. He will open another application, for example, a text document. At this moment an unpleasant surprise awaits us: if the system does not have enough resources to display this application, it may simply close ours without a warning! Therefore, the call will be terminated, leaving the user very confused.

Fortunately, there is a way to avoid this by using the Foreground Service mechanism. We mark our application as being actively used by the user even if it is minimized. After that, the application might get closed only in the most extreme case, if the system runs out of resources even for the most crucial processes.

The system, for security reasons, requires a persistent small notification, letting the user know that the application is performing work in the background.

It is essentially a normal notification, albeit with one difference: it can’t be swiped away. You don’t need to worry about accidentally wiping it away, so the application is once again defenseless against the all-optimizing system. 

You can do with a very small notification:

It appears quietly in the notification panel, without showing immediately to the user, like an incoming call notification. 

Nevertheless, it is still a notification, and all the techniques described in the previous paragraph apply to it – you can add buttons and customize the design

Picture-in-picture for video calls

Now the user can participate in a call or conference call and mind his own business without being afraid that the call will end abruptly. However, we can go even further in supporting multitasking! 

If your app has a video call feature, you can show a small video call window (picture-in-picture) for the user’s convenience, even if they go to other app screens. And, starting from Android 8.0, we can show such a window not only in our application but also on top of other applications!

You can also add controls to this window, such as camera switching or pause buttons. Read our guide on PiP here.

Ability to switch audio output devices

An integral part of any application with calls, video conferences, or broadcasts is audio playback. But how do we know from which audio output device the user wants to hear the sound? We can, of course, try to guess for him, but it’s always better to guess and provide a choice. For example, with this feature, the user won’t have to turn off the Bluetooth headphones to turn on the speakerphone

So if you give the user the ability to switch the audio output device at any point in the call, they will be grateful.

The implementation often depends on the specific application, but there is a method that works in almost all cases. We’ve described it here.

A deep link to quickly join a conference or a call

For both app distribution and UX, the ability to share a broadcast or invite someone to a call or conference is useful. But it may happen that the person invited is not yet a user of your app.

Well, that won’t be for long. You can generate a special link that will take those who already have the app directly to the call to which they were invited and those who don’t have the app installed to their platform’s app store. iPhone owners will go to the App Store, and Android users will go to Google Play. 

In addition, with this link, once the application is installed, it will launch immediately, and the new user will immediately get into the call to which he was invited!  Create your own deep links using our code examples.

Bottom line

We covered the main features of the system that allows us to improve the user experience when using our audio/video apps, from protecting our app from being shut down by the system right during a call, to UX conveniences like picture-in-picture mode.

Of course, every app is unique, with its own tasks and nuances, so these tips are no clear-cut rules. Nevertheless, if something from this list seems appropriate for a particular application, it’s worth implementing.