Skip to main content

Make a call to a mobile app

During this guide you will learn how to setup and use the Telnyx RTC Native SDKs into your app to receive calls through push notifications.

Requirements

| iOS | Android |


Build the iOS app

Configure the project

  1. Setup the TelnyxRTC iOS SDK into your project.

  2. Enable the following capabillities into your app:

    • Background mode: Audio, Airplay and Picture in picture
    • Background mode: Voice over IP
    • Push Notifications

Setup the Telnyx client

  1. Import the SDK into your ViewController
import TelnyxRTC
  1. Create and instance of the SDK
let client = TxClient()
  1. Setup the delegate
client.delegate = self
// MARK: - Callbacks from the Telnyx RTC SDK
extension ViewController: TxClientDelegate {
func onSocketConnected() {}

func onSocketDisconnected() {}

func onClientError(error: Error) {}

func onClientReady() {}

func onSessionUpdated(sessionId: String) {}

func onCallStateUpdated(callState: CallState, callId: UUID) {
DispatchQueue.main.async {
self.callStateLabel.text = "\(callState)"
}
}

// This method will be fired when receiving a call while the client is connected
func onIncomingCall(call: Call) {
guard let incomingCallUUID = call.callInfo?.callId else {
print("Unknwon incoming call..")
return
}

if let currentCallUUID = self.call?.callInfo?.callId {
//Hangup the previous call if there's one active
executeEndCallAction(uuid: currentCallUUID)
}

self.call = call

// Get the caller information
let calleer = self.call?.callInfo?.callerName ?? self.call?.callInfo?.callerNumber ?? "Unknown"

// Report the incoming call to CallKit
let callHandle = CXHandle(type: .generic, value: calleer)
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
callUpdate.hasVideo = false

self.callKitProvider.reportNewIncomingCall(with: incomingCallUUID, update: callUpdate) { error in
if let error = error {
print("Error reporting the incoming call to CallKit: \(error.localizedDescription)")
} else {
print("Incoming call successfully reported.")
}
}
}

func onRemoteCallEnded(callId: UUID) {
self.executeEndCallAction(uuid: callId)
}

// This method will be fired when receiving a call after connecting the client from a Push Notification
func onPushCall(call: Call) {
// Once the notification has been received and you have advised the Telnyx SDK about it.
// You will receive the new call here.
self.call = call
}
}

Setup CallKit

Use CallKit to integrate your calling services with other call-related apps on the system. CallKit provides the calling interface, and you handle the back-end communication with your VoIP service. For incoming and outgoing calls, CallKit displays the same interfaces as the Phone app, giving your app a more native look and feel. CallKit also responds appropriately to system-level behaviors such as Do Not Disturb.

For more information check the CallKit official documentation.

  1. Setup the CallKit provider and Controller
    var callKitProvider: CXProvider!
let callKitCallController = CXCallController()
func initCallKit() {
let configuration = CXProviderConfiguration()
configuration.maximumCallGroups = 1
configuration.maximumCallsPerCallGroup = 1
self.callKitProvider = CXProvider(configuration: configuration)
self.callKitProvider.setDelegate(self, queue: nil)
}
  1. Implement the CXProviderDelegate
extension ViewController: CXProviderDelegate {

func providerDidReset(_ provider: CXProvider) {}

func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
let callUUID = action.callUUID
self.call = try! self.client.newCall(callerName: self.kCallerName,
callerNumber: self.kCallerNumber,
destinationNumber: self.kDestination,
callId: callUUID)

// This step is important: It will fire the didActivate audioSession
// If we skip this process the audio is not going to flow
provider.reportOutgoingCall(with: callUUID, connectedAt: Date())
}

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
self.call?.answer()
action.fulfill()
}

func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
self.call?.hangup()
self.call = nil

action.fulfill()
}

func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
self.client.isAudioDeviceEnabled = true
}

func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
self.client.isAudioDeviceEnabled = false
}

}

Setup PushKit

If your app provides Voice-over-IP (VoIP) phone services, you may use PushKit to handle incoming calls on user devices. PushKit provides an efficient way to manage calls that doesn’t require your app to be running to receive calls. Upon receiving the notification, the device wakes up your app and gives it time to notify the user and connect to your call service.

For more information check the PushKit official documentation.

  1. Init PushKit to receive VoIP push notifications:
private var pushRegistry = PKPushRegistry.init(queue: DispatchQueue.main)
 func initPushKit() {
self.pushRegistry.delegate = self
self.pushRegistry.desiredPushTypes = Set([.voIP])
}
  1. Implement the PKPushRegistryDelegate. Inside this delegate you will receive updates from APNS of the device Push Token and the
extension ViewController: PKPushRegistryDelegate {

func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
// Here you will receive the APNS device token.
}

func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
// Invalidate the old token.
}

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
// Process the incoming VoIP push notifications
}
}
  1. Inside the didUpdate credentials delegate method it's required to:
    • Store your APNS token
    • Register the APNS device token into our backend by connecting the TelnyxClient as follows:
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
if (type == .voIP) {
// This push notification token has to be sent to Telnyx when connecting the Client.
let deviceToken = credentials.token.reduce("", {$0 + String(format: "%02X", $1) })
self.savePushToken(pushToken: deviceToken)

// Create the SDK configuration to use SIP credentials
let txConfig = TxConfig(sipUser: self.kSipUserName,
password: self.kSipUserPassword,
// Notice that we are passing the push device token here to register the device
pushDeviceToken: self.getPushToken())

// Register to listen SDK events
self.client.delegate = self

// Connect the client
try! client.connect(txConfig: txConfig)

debugPrint("Your APNS token is: \(deviceToken)")
}
}
  1. Process the incoming push notification. In this step we need to:
    • Decode the push payload
    • Send the push notificaiton payload to the SDK to get the call
    • Register the call into the system using CallKit.
// Call this function into the PushKit delegate method `didReceiveIncomingPushWith payload`
func handleVoIPPushNotification(payload: PKPushPayload) {
if let metadata = payload.dictionaryPayload["metadata"] as? [String: Any],
let callId = metadata["call_id"] as? String,
let callUUID = UUID(uuidString: callId),
let callerName = metadata["caller_name"] as? String,
let callerNumber = metadata["caller_number"] as? String {

// Advise the Telnyx Client that a PN has been received passing your login credentials
let txConfig = TxConfig(sipUser: self.kSipUserName,
password: self.kSipUserPassword,
pushDeviceToken: self.getPushToken())

try! client.processVoIPNotification(txConfig: txConfig)

// Report the incoming call to CallKit
let callHandle = CXHandle(type: .generic, value: callerName.isEmpty ? callerNumber : callerName)
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
callUpdate.hasVideo = false

self.callKitProvider.reportNewIncomingCall(with: callUUID, update: callUpdate) { error in
if let error = error {
print("Error reporting the incoming call to CallKit: \(error.localizedDescription)")
} else {
print("Incoming call successfully reported.")
}
}
} else {
debugPrint("Invalid push notification payload.")
}
}

Code flow summarized

  1. Initialize push kit to get an APNS token
  2. Send the APNS token to register the device into Telnyx backend by login in through the SDK using the user's SIP credentials.
  3. When a PN is received through PushKit, two main actions must be executed: a. Warn the Telnyx SDK about the incoming push notification by calling processVoIPNotification in order to get the call connected. b. Register the incoming call into CallKit. This will trigger the Incoming call system notification to be displayed.
  4. The SDK will fire the delegate method onPushCall with the new instance of the call that can be answered or rejected.

Build the Android app

Configure the project

  1. Setup the TelnyxRTC Android SDK into your project. The SDK is delivered through Jitpack.
  2. Add the following permissions into your project manifest:
android.permission.INTERNET
android.permission.RECORD_AUDIO
android.permission.MODIFY_AUDIO_SETTINGS

Setup the Telnyx client

  1. Initialize the client: To initialize the TelnyxClient you will have to provide the application context. Once an instance is created, you can call the .connect() method to connect to the socket. An error will appear as a socket response if there is no network available:
  telnyxClient = TelnyxClient(context)
telnyxClient.connect()
  1. Logging into Telnyx Client: To log into the Telnyx WebRTC client, you'll need to authenticate using a Telnyx SIP Connection. Follow this guide to create JWTs (JSON Web Tokens) to authenticate. To log in with a token we use the tokinLogin() method. You can also authenticate directly with the SIP Connection username and password with the credentialLogin() method:
 telnyxClient.tokenLogin(tokenConfig)
//OR
telnyxClient.credentialLogin(credentialConfig)

Note: tokenConfig and credentialConfig are data classes that represent login settings for the client to use. They look like this:

sealed class TelnyxConfig

/**
* Represents a SIP user for login - Credential based
*
* @property sipUser The SIP username of the user logging in
* @property sipPassword The SIP password of the user logging in
* @property sipCallerIDName The user's chosen Caller ID Name
* @property sipCallerIDNumber The user's Caller ID Number
* @property fcmToken The user's Firebase Cloud Messaging device ID
* @property ringtone The integer raw value of the audio file to use as a ringtone
* @property ringBackTone The integer raw value of the audio file to use as a ringback tone
* @property logLevel The log level that the SDK should use - default value is none.
* @property autoReconnect whether or not to reattempt (3 times) the login in the instance of a failure to connect and register to the gateway with valid credentials
*/
data class CredentialConfig(
val sipUser: String,
val sipPassword: String,
val sipCallerIDName: String?,
val sipCallerIDNumber: String?,
val fcmToken: String?,
val ringtone: Int?,
val ringBackTone: Int?,
val logLevel: LogLevel = LogLevel.NONE,
val autoReconnect : Boolean = true
) : TelnyxConfig()

/**
* Represents a SIP user for login - Token based
*
* @property sipToken The JWT token for the SIP user.
* @property sipCallerIDName The user's chosen Caller ID Name
* @property sipCallerIDNumber The user's Caller ID Number
* @property fcmToken The user's Firebase Cloud Messaging device ID
* @property ringtone The integer raw value of the audio file to use as a ringtone
* @property ringBackTone The integer raw value of the audio file to use as a ringback tone
* @property logLevel The log level that the SDK should use - default value is none.
* @property autoReconnect whether or not to reattempt (3 times) the login in the instance of a failure to connect and register to the gateway with a valid token
*/
data class TokenConfig(
val sipToken: String,
val sipCallerIDName: String?,
val sipCallerIDNumber: String?,
val fcmToken: String?,
val ringtone: Int?,
val ringBackTone: Int?,
val logLevel: LogLevel = LogLevel.NONE,
val autoReconnect : Boolean = true,
) : TelnyxConfig()
  1. Creating a call invitation: In order to make a call invitation, you need to provide your callerName, callerNumber, the destinationNumber (or SIP credential), and your clientState (any String value).
   telnyxClient.call.newInvite(callerName, callerNumber, destinationNumber, clientState)
  1. Accepting a call: In order to be able to accept a call, we first need to listen for invitations. We do this by getting the Telnyx Socket Response as LiveData:
  fun getSocketResponse(): LiveData<SocketResponse<ReceivedMessageBody>>? =
telnyxClient.getSocketResponse()

We can then use this method to create a listener that listens for an invitation - in this example, we assume getSocketResponse is a method within a ViewModel.

 mainViewModel.getSocketResponse()
?.observe(this, object : SocketObserver<ReceivedMessageBody>() {
override fun onConnectionEstablished() {
// Handle a succesfully established connection
}

override fun onMessageReceived(data: ReceivedMessageBody?) {
when (data?.method) {
SocketMethod.CLIENT_READY.methodName -> {
// Fires once client has correctly been setup and logged into, you can now make calls.
}

SocketMethod.LOGIN.methodName -> {
// Handle a successful login - Update UI or Navigate to new screen, etc.
}

SocketMethod.INVITE.methodName -> {
// Handle an invitation Update UI or Navigate to new screen, etc.
// Then, through an answer button of some kind we can accept the call with:
val inviteResponse = data.result as InviteResponse
mainViewModel.acceptCall(inviteResponse.callId, inviteResponse.callerIdNumber)
}

SocketMethod.ANSWER.methodName -> {
//Handle a received call answer - Update UI or Navigate to new screen, etc.
}

SocketMethod.BYE.methodName -> {
// Handle a call rejection or ending - Update UI or Navigate to new screen, etc.
}
}
}

override fun onLoading() {
// Show loading dialog
}

override fun onError(message: String?) {
// Handle errors - Update UI or Navigate to new screen, etc.
}

})

When we receive a call we will receive an InviteResponse data class that contains the details we need to accept the call. We can then call the acceptCall method in TelnyxClient from our ViewModel:

 telnyxClient.call.acceptCall(callId, destinationNumber)

Adding Push Notifications

Logging in with a Push Notification Token

With the token received from the firebase servers, we can now tell Telnyx which device to push notifications to that are associated with your connection. To do so we simply include the firebase token with the initial login message:

telnyxClient = TelnyxClient(context)
telnyxClient.connect()



val credentalConfig = CredentialConfig(
sipUsername,
password,
sipCallerName,
sipCallerNumber,
fcmToken, // The token received from the firebase call we did earlier
ringtone,
ringBackTone,
LogLevel.ALL
)
telnyxClient.credentialLogin(credentialConfig)

Once this log in is successful, and you have received the CLIENT_READY socket message, you have successfully assigned this device to the credential and any calls received while not connected to the socket will result in a Push Notification.

Receiving a Push Notification

In order to actually receive a Push Notification sent to this device, the final step is to implement a MessagingService class which will be able to receive incoming messages sent by Firebase and display them as a notification.

First create a class that implements the FirebaseMessagingService() class. Below is a barebones boilerplate implementation:

class MyMessagingService : FirebaseMessagingService() {

/**
* Called when message is received.
*
* @param remoteMessage Object representing the message received from Firebase Cloud Messaging.
*/
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)

}

/**
* Called if Instance ID token is updated. This may occur if the security of
* the previous token had been compromised. Note that this is called when the Instance ID token
* is initially generated so this is where you would retrieve the token.
*/
override fun onNewToken(token: String) {

}
}

There are a few methods we can override from this class, but the important ones for this use case are onMessageRecieved, which will fire when we are sent a Push Notification and will contain the contents of the Telnyx Push notification inside the RemoteMessage in the following format:

data class PushMetaData(
val caller_name: String,
val caller_number: String,
val call_id: String
)

The other method is the onNewToken method, which fires when our token changes for whatever reason. This new token can be used to re-register. You can use this method to persist tokens with third-party servers.

Displaying a Push Notification

Now that we can receive messages from Firebase, we can flesh out the onMessageReceived override method to show a notification when we receive a messages.

Applications that are targeting SDK 6 or above (Android O) must implement notification channels and add its notifications to at lease one of them. The idea behind notification channels is that apps can group different types of notifications into “channels.” Each channel can then be turned on or off by the user. This all happens in the Android settings. For each channel, you can set the visual and auditory behavior that is applied to all notifications in that channel.

So first, within onMessageReceived, let’s setup our Telnyx Notification Channel, providing a name, description and some features we would like to include with the channel such as whether or not to vibrate, LED colour of notifications, etc.

override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)

val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setupChannels(notificationManager)
}

}

@RequiresApi(api = Build.VERSION_CODES.O)
private fun setupChannels(notificationManager: NotificationManager?) {
val adminChannelName = "Telnyx Notification Channel"
val adminChannelDescription = "Channel to receive Telnyx Notifications"

val adminChannel = NotificationChannel(
“telnyx_channel_id”,
adminChannelName,
NotificationManager.IMPORTANCE_HIGH
)
adminChannel.description = adminChannelDescription
adminChannel.enableLights(true)
adminChannel.lightColor = Color.RED
adminChannel.enableVibration(true)
notificationManager?.createNotificationChannel(adminChannel)
}

Now that we have successfully created a dedicated channel to receive Telnyx related Notifications, next we create two Result Intents for 2 actions on the notification we are going to build. The first is the answerResultIntent, for when the call notification answer button is clicked, and rejectResultIntent, for when the call notification reject button is clicked.

You can read more about Result Intents here, but essentially these intents will tell our MainActivity to do something when clicked

override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)

val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setupChannels(notificationManager)
}

val rejectResultIntent = Intent(this, MainActivity::class.java)
rejectResultIntent.addCategory(Intent.CATEGORY_LAUNCHER)
rejectResultIntent.action = Intent.ACTION_VIEW
rejectResultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
rejectResultIntent.putExtra(EXT_KEY_DO_ACTION, ACT_REJECT_CALL)
val rejectPendingIntent = PendingIntent.getActivity(
this,
REJECT_REQUEST_CODE,
rejectResultIntent,
PendingIntent.FLAG_IMMUTABLE
)

val answerResultIntent = Intent(this, MainActivity::class.java)
answerResultIntent.addCategory(Intent.CATEGORY_LAUNCHER)
answerResultIntent.action = Intent.ACTION_VIEW
answerResultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
answerResultIntent.putExtra(EXT_KEY_DO_ACTION, ACT_ANSWER_CALL)
val answerPendingIntent = PendingIntent.getActivity(
this,
ANSWER_REQUEST_CODE,
answerResultIntent,
PendingIntent.FLAG_IMMUTABLE
)

}

Note: you can add certain flags to explain how the interaction with the MainActivity operates. In this case we have added FLAG_ACTIVITY_CLEAR_TOP and FLAG_ACTIVITY_SINGLE_TOP.

With these two result intents setup, we can now show a notification with them applied, and when they are clicked we can open the MainActivity and run specific functionality based on the action (ACT_ANSWER_CALL, ACT_REJECT_CALL) contained in each intent.

Let’s build or notification, applying an icon, title, content text, priority and vibrate pattern:

override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)





val notificationID = Random().nextInt(3000)

val params = remoteMessage.data
val objects = JSONObject(params as Map<*, *>)
val metadata = objects.getString("metadata")
val gson = Gson()
val telnyxPushMetadata = gson.fromJson(metadata, PushMetaData::class.java)

val notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(this, TELNYX_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_contact_phone)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentTitle(remoteMessage.data["title"])
.setContentText(telnyxPushMetadata.caller_name + " - " + telnyxPushMetadata.caller_number)
.setVibrate(longArrayOf(1000, 1000, 1000, 1000, 1000))
.addAction(R.drawable.ic_call_white, ACT_ANSWER_CALL, answerPendingIntent)
.addAction(R.drawable.ic_call_end_white, ACT_REJECT_CALL, rejectPendingIntent)
.setAutoCancel(true)
.setSound(notificationSoundUri)

notificationManager.notify(notificationID, notificationBuilder.build())

Now let’s finally register this new service within the application tags in our manifest so that the methods can be called.

  <application
android:allowBackup="true"
android:icon="@mipmap/ic_app_icon"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_app_icon_round"
android:supportsRtl="true"
android:name=".App"
android:theme="@style/Theme.TelnyxAndroidWebRTCSDK">

<service
android:name=".utility.MyMessagingService"
android:priority="10000"
android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter>
</service>

We have now done everything we need to receive a push notification, if we receive a call while not connected to a socket, a notification will appear in our system tray with the details set within the notification builder.

Reacting to a notification

Previously we created two result intents, one for accepting a call and the other to reject the call. How do we turn these intents into functional code that actually does something when pressed?

These actions are sent as intents containing actions in our MainActivity when pressed. A rudimentary implementation could be like this within MainActivity:

class MainActivity : AppCompatActivity() {





private fun handleCallNotification() {
val action = intent.extras?.get(MyMessagingService.EXT_KEY_DO_ACTION) as String?

action?.let {
if (action == MyMessagingService.ACT_ANSWER_CALL) {
// Handle Answer, log into socket with stored credentials, wait for invitation and accept the call
} else if (action == MyMessagingService.ACT_REJECT_CALL) {
// Handle Reject, log into socket with stored credentials, wait for invitation and reject the call

}
}
}

override fun onResume() {
super.onResume()
handleCallNotification()
}

}

We are overriding onResume and calling a method to handle call notifications. The handleCallNotification() method grabs the chosen action pressed in the notification, checks which action it is (ACT_ANSWER_CALL or ACT_REJECT_CALL) and does some logic based on the chosen action.