Signed Call Android SDK

Learn how to integrate Signed Call Android SDK in your app to avail the Signed Call feature.

Overview

CleverTap provides In-App calls via its Signed Call Android SDK, which means you can make and receive calls in any Android application if the device has an internet connection and Signed Call Android SDK. This document shows you how to integrate the Signed Call Android SDK and manage calls. To know more about the Signed Call feature, refer to Signed Call.

Requirements

The basic requirements for the Signed Call Android SDK are:

  • Minimum SDK Level - 21
  • Java 8 and above
  • Application permissions for the following:
    • Microphone (Required)
    • Notification (Required for Android 13 and above)
    • Read Phone State (Optional)

📘

Emulator Support

Emulator support is available for voice calls, but voice transmission will not work.

Install Signed Call Android SDK

Download the latest version of the Signed Call Android SDK from mavenCentral as follows:

  1. Include mavenCentral in your project-level build.gradle file as follows:
allprojects {
    repositories {
        mavenCentral()
    }
}
  1. You must use the specified SDK versions for the corresponding SDK features.
    1. To use P2P calls, include SDK version 0.0.5.7 with the following line in your application module's build.gradle file:
      implementation "com.clevertap.android:clevertap-signedcall-sdk:0.0.5.7"
      
    2. To use the M2P campaigns feature, include SDK version 0.0.6.2-m2p-beta with the following line in your application module's build.gradle file:
      implementation "com.clevertap.android:clevertap-signedcall-sdk:0.0.6.2-m2p-beta"
      

📘

SDK Private Beta Release

The SDK version 0.0.6.2-m2p-beta is intended to support the M2P Campaign feature, which is currently in private beta. It is recommended to use SDK version v0.0.5.7 if you want to use only P2P calls.

Signed Call SDK Dependencies

To enable voice calling with the Signed Call Android SDK, you must add the following dependencies to your application modulesbuild.gradle file:

CleverTap Android SDK

The Signed Call Android SDK uses CleverTap Android SDK for analytics. The Signed Call Android SDK requires an active CleverTapAPI instance as a parameter during the SDK initialization.

To integrate the CleverTap Android SDK, refer to the CleverTap Android SDK Integration.

📘

Minimum Supported Version

The Signed Call Android SDK integrates with CleverTap SDK v5.2.0 or higher.

FCM

The Signed Call Android SDK uses FCM dependency to get the FCM token of the device required for the successful SDK initialization. This FCM token is then used by Signed Call to enable calls via FCM Based Call Routing Channel.

Perform the following steps to add the FCM dependency to your application:

  1. Refer to the Firebase Integration Guide to add Firebase to your project if you have not already added it.
  2. Add the following code to the application module's dependency element:
implementation 'com.google.firebase:firebase-messaging:21.0.0'

📘

FCM Version

For Signed Call Android SDK, the minimum supported version of FCM is v21.0.0.

Socket-IO Client

The Signed Call Android SDK uses a Socket-IO client library to enable the socket-based signaling channel for voice calls.

To add the Socket-IO client dependency to your application, add the following line to the application module's dependency element:

implementation('io.socket:socket.io-client:2.1.0') {
        exclude group: 'org.json', module: 'json'
}

Glide

The Signed Call Android SDK uses a Glide library for loading the image assets on the call screen.

To add the Glide dependency to your application, add the following line to the application module's dependency element:

implementation 'com.github.bumptech.glide:glide:4.12.0'

Work Manager

The Signed Call Android SDK uses a Work Manager dependency to process the incoming call push for the receiver.

To add the Work Manager dependency to your application, add the following line to the application module's dependency element:

implementation 'androidx.work:work-runtime:2.7.1'

ConstraintLayout

The Signed Call Android SDK uses a ConstraintLayout dependency to build a responsive UI for the call screens.

To add the ConstraintLayout dependency to your application, add the following line to the application module's dependency element:

implementation 'androidx.constraintlayout:constraintlayout:2.1.3'

Upgrade Java Compatibility

The Signed Call Android SDK's source and target compatibility are set to Java 8.
To upgrade your application to target Java 8, use the following snippet:

android {
	compileOptions {
		sourceCompatibility JavaVersion.VERSION_1_8
		targetCompatibility JavaVersion.VERSION_1_8
	}
}

After updating your build.gradle file, sync your project by clicking the Sync Project button.

Initialize Signed Call Android SDK

Initialize the Signed Call Android SDK using the SignedCallAPI class instance:

SignedCallInitResponse signedCallInitListener = new SignedCallInitResponse() {
      @Override
      public void onSuccess() {
          //App is notified on the main thread when the Signed Call SDK is initialized
      }

      @Override
      public void onFailure(@NonNull InitException initException) {
          //App is notified on the main thread when the initialization is failed
          Log.d("SignedCall: ", "error code: " + initException.getErrorCode()
                 + "\n error message: " + initException.getMessage()
                 + "\n error explanation: " + initException.getExplanation());

          if (initException.getErrorCode() == InitException.SdkNotInitializedException.getErrorCode()) {
             //Handle this error here
          }
      }
};

//Create a Builder instance of SignedCallInitConfiguration and pass it inside the init() method
SignedCallInitConfiguration initConfiguration = new SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
         .build();

SignedCallAPI.getInstance().init(getApplicationContext(), initConfiguration, cleverTapAPI, signedCallInitListener);
val signedCallInitListener: SignedCallInitResponse = object : SignedCallInitResponse {
    override fun onSuccess() {
        //App is notified on the main thread when the Signed Call SDK is initialized
    }

    override fun onFailure(initException: InitException) {
        //App is notified on the main thread when the initialization is failed
        Log.d("SignedCall: ", "error code: " + initException.errorCode
                 + "\n error message: " + initException.message
                 + "\n error explanation: " + initException.explanation)

        if (initException.errorCode == InitException.SdkNotInitializedException.errorCode) 
        {
           //Handle this error here
        }
    }
}

//Create a Builder instance of SignedCallInitConfiguration and pass it inside the init() method
val initConfiguration = SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
    .build()

SignedCallAPI.getInstance().init(applicationContext, initConfiguration, cleverTapAPI, signedCallInitListener)

Following are the parameters passed inside the constructor of SignedCallInitConfiguration.Builder:

initOptions

The initOptions is a JSON object that is passed inside the constructor of SignedCallInitConfiguration.Builder with the following properties:

Property

Description

Type

Notes

accountId

  • Unique identity of the client's account.
  • Available from the CleverTap dashboard.

String

Required

apiKey

  • Unique identity of the client's account.
  • Available from the CleverTap dashboard.

String

Required

cuid

Unique identity of the user.

String

Required

appId

  • Application ID of the app.
  • Use the BuildConfig.APPLICATION_ID to get your application ID.

String

Optional

name

  • Name of the user.
  • The name must range between 3 and 25 characters.
StringOptional
ringtoneThe URL of the ringtone played on the caller's end during the outgoing call.
Note: The default ringtone plays without this parameter.
StringOptional

📘

CUID Validation Rules

The following are the validation rules for cuid:

  • Must range between 5 and 50 characters, starting from v0.0.5; otherwise, it should be less than 15 characters.
  • Must start with either alphabet or number.
  • Must be alphanumeric, should contain at least 1 alphabet.
  • The name is case-sensitive, and only '_' is allowed as a special character.
  • The cuid parameter cannot be of type number-number, that is, a number followed by a special character, which is again followed by another number. For example, org_25 is allowed, but 91_8899555 is not permitted.
  • Must be unique for every user.
  • Must be unique for every device to allow multiple logins for the user from different devices. In such cases, the user will have multiple cuid's.

The syntax for initOptions is as follows:

JSONObject initOptions = new JSONObject();
try {
    initOptions.put("accountId", <string>);
    initOptions.put("apiKey", <string>);
    initOptions.put("cuid", <string>);
    initOptions.put("appId", <string / optional>);
    initOptions.put("name", <string / optional>);
    initOptions.put("ringtone", <string / optional>);
} catch (JSONException e) {
    e.printStackTrace();
}
val initOptions = JSONObject();
try {
    initOptions.put("accountId", <string>);
    initOptions.put("apiKey", <string>);
    initOptions.put("cuid", <string>);
    initOptions.put("appId", <string / optional>);
    initOptions.put("name", <string / optional>);
    initOptions.put("ringtone", <string / optional>);
} catch (e: JSONException) {
    e.printStackTrace();
}

allowPersistSocketConnection

The socket connection plays a crucial role in processing the call request to initiate a call and receive calls on the socket channel. Android OS imposes several battery restrictions that lead to issues in maintaining a persistent socket connection. To overcome this issue, Signed Call Android SDK expects the allowPersistSocketConnection flag, a boolean parameter, to be passed inside the SignedCallInitConfiguration.Builder constructor.

The following is the Signed Call Android SDK behavior:

ValueSocket ConnectionSigned Call Android SDK Behavior
truePersistentUses a background service to keep the socket connection persistent in the foreground and background states of the application.
falseNon-persistentDoes not use the background service for the socket connection; hence the socket connection may be inconsistent under battery restriction scenarios.

📘

Recommended Settings for Android 11 and Above

For Android 11 and above, if the socket connection is persistent, the application might show a system-generated notification to the user that the application is draining the device's battery. We recommend setting the value as false and keeping the socket connection non-persistent for transactional businesses.

SDK Initialization Modes

Starting from SDK version 0.0.6.2-m2p-beta, the CleverTap Signed Call SDK supports two initialization modes: FULL_FEATURE_MODE and M2P_FEATURE_MODE. These modes define the behavior of the SDK to enable the capability based on the mode.

The following is the Signed Call Android SDK behavior based on mode value:

ValueSigned Call Android SDK Behavior
SCInitMode.M2P_FEATURE_MODEThis is a default mode. This mode disables P2P calls and enables M2P(Machine-to-Person) campaign calls.
SCInitMode.FULL_FEATURE_MODEIt enables P2P (Person-to-Person) as well as M2P(Machine-to-Person) calls. Using this mode with M2P beta SDK versions in the production environment is not recommended.

The Signed Call Android SDK exposes a setInitMode(mode) method via the SignedCallInitConfiguration.Builder class. Pass the mode type to either SCInitMode.FULL_FEATURE_MODE or SCInitMode.M2P_FEATURE_MODE as below:

SignedCallInitConfiguration initConfiguration = new SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
         .setInitMode(SCInitMode.FULL_FEATURE_MODE | SCInitMode.M2P_FEATURE_MODE)
         .build();

SignedCallAPI.getInstance().init(getApplicationContext(), initConfiguration, cleverTapAPI, signedCallInitListener);
val initConfiguration = SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
        .setInitMode(SCInitMode.CALL_MODE | SCInitMode.CAMPAIGN_MODE)
        .build()

SignedCallAPI.getInstance().init(applicationContext, initConfiguration, cleverTapAPI, signedCallInitListener)

Configure Machine-to-Person (M2P) Feature

The Signed Call Android SDK runs a foreground service to communicate with the server before the actual call and fetches information about the campaign meta and flow. During this process, the SDK displays a foreground notification to the user. After processing, the SDK replaces the foreground notification with the incoming call notification.

Note that the foreground service keeps the application in the wake state, reducing the chances of communication failures due to network or device restrictions imposed by the Android OS or OEM.

The Signed Call Android SDK exposes a setM2PConfiguration(M2PConfiguration config) method via the SignedCallInitConfiguration.Builder class that expects an instance of the M2PConfiguration class as follows:

M2PNotificationClickListener m2pNotificationClickListener = (context, m2PCallOptions) -> {
     Log.d(LOG_TAG, "M2P Notification is Clicked! " + m2PCallOptions);
 };

M2PCancelCtaClickListener m2pCancelCtaClickListener = (context, m2PCallOptions) -> {
     Log.d(LOG_TAG, "M2P Cancel CTA is Clicked!" + m2PCallOptions);
};

M2PConfiguration m2pConfiguration;
try {
   M2PNotification m2pNotification = new M2PNotification(title, subtitle);
   m2pNotification.setLargeIcon(largeIcon);
   m2pNotification.setCancelCtaLabel(cancelCta);
   m2pNotification.registerClickListener(m2pNotificationClickListener);
   m2pNotification.registerCancelCtaClickListener(m2pCancelCtaClickListener);
   m2pConfiguration = new M2PConfiguration.Builder(m2pNotification)
            build();
} catch (Exception e) {
   e.printStackTrace();
}

SignedCallInitConfiguration initConfiguration = new SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
         .setM2PConfiguration(m2pConfiguration)
         .build();

SignedCallAPI.getInstance().init(getApplicationContext(), initConfiguration, cleverTapAPI, signedCallInitListener);
val m2pNotificationClickListener: M2PNotificationClickListener = M2PNotificationClickListener { context, m2PCallOptions ->
    Log.d(LOG_TAG, "M2P Notification is Clicked! $m2PCallOptions")
}

val m2pCancelCtaClickListener: M2PCancelCtaClickListener = M2PCancelCtaClickListener { context, m2PCallOptions ->
    Log.d(LOG_TAG, "M2P Cancel CTA is Clicked! $m2PCallOptions")
}

val m2pConfiguration: M2PConfiguration
try {
    val m2pNotification = M2PNotification(m2pTitle, m2pSubtitle)
    m2pNotification.largeIcon = m2pLargeIcon
    m2pNotification.cancelCtaLabel = cancelCta
    m2pNotification.registerClickListener(m2pNotificationClickListener)
    m2pNotification.registerCancelCtaClickListener(m2pCancelCtaClickListener)
    m2pConfiguration = M2PConfiguration.Builder(m2pNotification)
            .build()
} catch (e: Exception) {
    e.printStackTrace()
}

val initConfiguration = SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
         .enableM2PFeature(m2pConfiguration)
         .build()

SignedCallAPI.getInstance().init(applicationContext, initConfiguration, cleverTapAPI, signedCallInitListener)

📘

Note

As the M2P feature is enabled by default in M2P beta versions of the SDK, the above implementation is mandatory when using it.

Follow the table below to understand the usage of various parameters passed to the M2PNotification instance:

PropertyDescriptionTypeNotes
title
  • Title text, limited to 65 characters..
StringRequired
subtitle
  • Subtitle text, limited to 240 characters.
StringRequired
largeIcon
  • ID of a drawable resource.
intOptional
registerClickListener
  • Property that registers a listener to receive the click events when the M2P notification is clicked.
M2PNotificationClickListenerOptional
registerCancelCtaClickListener
  • Property that registers a listener to receive an event when the user cancels the M2P call by clicking on cancel CTA.
M2PCancelCtaClickListenerOptional

Android 13 Changes

All applications having Android 13 and above must request Runtime Notification Permission from the user before sending the notifications.

Signed Call Android SDK utilizes both local and remote notifications during VoIP calls. Therefore, notification permission is required from the user to initialize the Signed Call SDK.

📘

Notification Permission Now Optional

Signed Call Android versions starting v0.0.5.5 to v0.0.6-m2p-beta, include an API that allows developers to choose whether notification permissions are optional or required for SDK initialization. For detailed instructions on making notification permission optional, refer to Configure Notification Permission as Optional.

📘

Signed Call Android SDK Support for Android 13

If you want to increase the target API of your application to 33 or higher, you must upgrade the following:

  • Signed Call Android SDK to version 0.0.2 or higher
  • CleverTap Android SDK to version 4.7.0 or higher

Push Primer for Push Notification Permission

The Push Primer is a local In-App notification that educates users about the context of the notifications before requesting the notification permission.

To initialize the Signed Call Android SDK for Android 13 and above, you can enable the Push Primer using either of the following:

Enable Push Primer via CleverTap Android SDK

CleverTap Android SDK v4.7.0 and above supports Push Primer. For more details, refer to Android Push Primer.

Enable Push Primer via Signed Call Android SDK

The Signed Call Android SDK enables you to display Push Primer using the promptPushPrimer(jsonObjectConfig) method through the SignedCallInitConfiguration.Builder class. It ensures the initialization happens after the notification permission is granted using Push Primer.

To configure the Push Primer:

  1. Create a Push Primer configuration using the In-App campaign's Half-Interstitial or Alert template.
//Creates push primer config using Half-Interstitial template
JSONObject jsonObjectConfig = CTLocalInApp.builder()
        .setInAppType(CTLocalInApp.InAppType.HALF_INTERSTITIAL)
        .setTitleText("Get Notified")
        .setMessageText("Please enable notifications on your device to use Push Notifications.")
        .followDeviceOrientation(true)
        .setPositiveBtnText("Allow")
        .setNegativeBtnText("Cancel")
        .setBackgroundColor(Constants.WHITE)
        .setBtnBorderColor(Constants.BLUE)
        .setTitleTextColor(Constants.BLUE)
        .setMessageTextColor(Constants.BLACK)
        .setBtnTextColor(Constants.WHITE)
        .setImageUrl("https://icons.iconarchive.com/icons/treetog/junior/64/camera-icon.png")
        .setBtnBackgroundColor(Constants.BLUE)
        .build();


//Creates push primer config using Alert template
JSONObject jsonObjectConfig = CTLocalInApp.builder()
        .setInAppType(CTLocalInApp.InAppType.ALERT)
        .setTitleText("Get Notified")
        .setMessageText("Enable Notification permission")
        .followDeviceOrientation(true)
        .setPositiveBtnText("Allow")
        .setNegativeBtnText("Cancel")
        .build();
//Creates push primer config using Half-Interstitial template
val jsonObjectConfig = CTLocalInApp.builder()
            .setInAppType(InAppType.HALF_INTERSTITIAL)
            .setTitleText("Get Notified")
            .setMessageText("Please enable notifications on your device to use Push Notifications.")
            .followDeviceOrientation(true)
            .setPositiveBtnText("Allow")
            .setNegativeBtnText("Cancel")
            .setBackgroundColor(Constants.WHITE)
            .setBtnBorderColor(Constants.BLUE)
            .setTitleTextColor(Constants.BLUE)
            .setMessageTextColor(Constants.BLACK)
            .setBtnTextColor(Constants.WHITE)
            .setImageUrl("https://icons.iconarchive.com/icons/treetog/junior/64/camera-icon.png")
            .setBtnBackgroundColor(Constants.BLUE)
            .build()


//Creates push primer config using Alert template
val jsonObjectConfig = CTLocalInApp.builder()
            .setInAppType(InAppType.ALERT)
            .setTitleText("Get Notified")
            .setMessageText("Enable Notification permission")
            .followDeviceOrientation(true)
            .setPositiveBtnText("Allow")
            .setNegativeBtnText("Cancel")
            .build()
  1. Pass the Push Primer configuration inside the promptPushPrimer(jsonObjectConfig) method of the SignedCallInitConfiguration.Builder class.
SignedCallInitConfiguration initConfiguration = new SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
         .promptPushPrimer(jsonObjectConfig)
         .build();

SignedCallAPI.getInstance().init(getApplicationContext(), initConfiguration, cleverTapAPI, signedCallInitListener);
val initConfiguration = SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
        .promptPushPrimer(jsonObjectConfig)
        .build()

SignedCallAPI.getInstance().init(applicationContext, initConfiguration, cleverTapAPI, signedCallInitListener)

📘

Note

  • The above configuration enables the Push Primer only if the device and application both target Android 13 (API level 33) or higher.
  • To obtain the result of the push notification permission request, the Signed Call SDK registers a listener. After registration, this listener continues monitoring the permission result, even if the Push Primer prompt is displayed from the CleverTap Android SDK.
  • If the notification permission is denied, the Signed Call Android SDK returns InitException.NotificationPermissionRequiredException within the onFailure(initException) method.
  • The initialization of the Signed Call Android SDK with the Push Primer configuration mentioned above should be invoked within the onResume lifecycle method of the Activity.

Configure Notification Permission as Optional

Starting with Android 13, the Signed Call Android SDK requires notification permission to enable call notifications, allowing users to interact with and return to calls.

The SDK requires this permission for initialization by default. If it is not provided, the SDK returns an exception -InitException.NotificationPermissionRequiredException through the onFailure(initException) method.

To make notification permission optional during SDK initialization, use the setNotificationPermissionRequired(boolean) method available in the SignedCallInitConfiguration.Builder class. Pass the boolean flag to false as shown below:

SignedCallInitConfiguration initConfiguration = new SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
         .setNotificationPermissionRequired(<pass boolean here>)
         .build();

SignedCallAPI.getInstance().init(getApplicationContext(), initConfiguration, cleverTapAPI, signedCallInitListener)
val initConfiguration = SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
        .setNotificationPermissionRequired(<pass boolean here>)
        .build()

SignedCallAPI.getInstance().init(applicationContext, initConfiguration, cleverTapAPI, signedCallInitListener)

📘

Expected SDK Behaviour without Notification Permission

  • Avoid making notification permissions as optional. Without this permission, the SDK can only display the call screen on Android 13 and above when the app is in the foreground. Otherwise, incoming calls will be automatically declined, triggering the VoIPCallStatus.CALL_DECLINED_DUE_TO_NOTIFICATIONS_DISABLED event in the callStatus(callStatusDetails) callback method.
  • If you decide to set notification permissions as non-mandatory, implementing the callStatus(callStatusDetails) callback method is essential.

Permission Management

Following are the permissions that the Signed Call Android SDK uses and their management:

Microphone

This is a required permission. The Signed Call Android SDK requires microphone permission to exchange voices during the call. At the receiver's end, the Signed Call Android SDK asks for microphone permission and handles it accordingly when the receiver answers the call. We recommend you add the required handling to request the microphone permission before initiating a call.

📘

Note

Starting from Signed Call Android SDK v0.0.3, microphone permission prompt limit displayed during a VoIP call at receiver is aligned with the permissible threshold set by Android platform. Previously, the Signed Call Android SDK blocked all incoming calls at the receiver's end if the microphone permission was denied even once.

Read Phone State

This is optional permission. The Signed Call Android SDK uses this permission to enable busy handling of public switched telephone network (PSTN) calls. This permission determines if the receiver is available or busy on a PSTN call. We recommend you add the required handling to request the Read Phone State permission before initiating a call.

The Signed Call Android SDK exposes a promptReceiverReadPhoneStatePermission(boolean) method via the SignedCallInitConfiguration.Builder class. Pass the boolean flag as true to allow the Signed Call Android SDK to prompt for the read phone state permission at the receiver's end when the receiver answers the call.

SignedCallInitConfiguration initConfiguration = new SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
         .promptReceiverReadPhoneStatePermission(<pass boolean here>)
         .build();

SignedCallAPI.getInstance().init(getApplicationContext(), initConfiguration, cleverTapAPI, signedCallInitListener)
val initConfiguration = SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
        .promptReceiverReadPhoneStatePermission(<pass boolean here>)
        .build()

SignedCallAPI.getInstance().init(applicationContext, initConfiguration, cleverTapAPI, signedCallInitListener)

Bluetooth Connect Permission

This is an optional permission but is recommended for a better user experience. The Signed Call Android SDK has built-in Bluetooth management support. However, enabling this feature on Android 12 and onwards requires Bluetooth Connect permission.

The Signed Call Android SDK uses this permission to enable communication with the paired Bluetooth device for audio management on Android 12 and onwards. To optimize the call experience, we recommend you request Bluetooth Connect permission.

Logout the Signed Call Android SDK

When the Signed Call Android SDK initializes, it maintains the init configuration in a local session. Use the logout(context) method to reset the active session and disable the Signed Call functionality (call initiation and reception).

SignedCallAPI.getInstance().logout(getApplicationContext());
SignedCallAPI.getInstance().logout(applicationContext)

Make a Signed Call (P2P Feature)

Use the following code to make a Signed Call:

OutgoingCallResponse outgoingCallResponseListener = new OutgoingCallResponse() {
    @Override
    public void onSuccess() {
        //App is notified on the main thread when the call-request is accepted and being processed by the signalling channel
    }

    @Override
    public void onFailure(CallException callException) {
        //App is notified on the main thread when the call is failed
        Log.d("SignedCall: ", "error code: " + callException.getErrorCode()
               + "\n error message: " + callException.getMessage()
               + "\n error explanation: " + callException.getExplanation());

        if (callException.getErrorCode() == CallException.BadNetworkException.getErrorCode()) {
            //Handle this error here
        }
    }
   
   // IMPORTANT:
   // Following callback method is no longer supported starting from v0.0.5.
   // Please use the new public API `registerVoIPCallStatusListener(SCVoIPCallStatusListener callStatusListener)`.
    @Override
    public void callStatus(VoIPCallStatus callStatus) {
        //App is notified on the main thread to notify the changes in the call-state
        if (callStatus == VoIPCallStatus.CALL_CANCELLED) {
            //when the call is cancelled from the initiator's end
        } else if (callStatus == VoIPCallStatus.CALL_DECLINED) {
            //when the call is declined from the receiver's end
        } else if (callStatus == VoIPCallStatus.CALL_MISSED) {
            //when the call is missed at the receiver's end
        } else if (callStatus == VoIPCallStatus.CALL_ANSWERED) {
            //When the call is picked up by the receiver
        } else if (callStatus == VoIPCallStatus.CALL_IN_PROGRESS) {
            //when the connection to the receiver is established
        } else if (callStatus == VoIPCallStatus.CALL_OVER) {
            //when the call has been disconnected
        } else if (callStatus == VoIPCallStatus.CALLEE_BUSY_ON_ANOTHER_CALL) {
            //when the receiver is busy on another call
        }
    }
};

SignedCallAPI.getInstance().call(getApplicationContext(), receiverCuid, contextOfCall, callOptions, outgoingCallResponseListener);
val outgoingCallResponseListener: OutgoingCallResponse = object : OutgoingCallResponse {
    override fun callStatus(callStatus: VoIPCallStatus) {
        //App is notified on the main thread to notify the changes in the call-state
        if (callStatus == VoIPCallStatus.CALL_CANCELLED) {
            //when the call is cancelled from the initiator's end
        } else if (callStatus == VoIPCallStatus.CALL_DECLINED) {
            //when the call is declined from the receiver's end
        } else if (callStatus == VoIPCallStatus.CALL_MISSED) {
            //when the call is missed at the receiver's end
        } else if (callStatus == VoIPCallStatus.CALL_ANSWERED) {
            //When the call is picked up by the receiver
        } else if (callStatus == VoIPCallStatus.CALL_IN_PROGRESS) {
            //when the connection to the receiver is established
        } else if (callStatus == VoIPCallStatus.CALL_OVER) {
            //when the call has been disconnected
        } else if (callStatus == VoIPCallStatus.CALLEE_BUSY_ON_ANOTHER_CALL) {
            //when the receiver is busy on another call
        }
    }

    override fun onSuccess() {
        //App is notified on the main thread when the call-request is accepted and being processed by the signalling channel
    }

    override fun onFailure(callException: CallException) {
        //App is notified on the main thread when the call is failed
        Log.d("SignedCall: ", "error code: ${callException.errorCode}" 
               + "\n error message: ${callException.message}" 
               + "\n error explanation: ${callException.explanation}")
    }
}


SignedCallAPI.getInstance().call(applicationContext, receiverCuid, contextOfCall, callOptions, outgoingCallResponseListener)

The parameters to make a Signed Call are as follows:

ParameterDescriptionTypeNotes
receiverCuidIt is the receiver's cuid. Note: The Signed Call Android SDK returns CallException.CalleeInfoRequiredException error if this parameter is not passed.StringRequired
contextOfCallIt specifies the context of the call. For example, Delivery Partner is calling, Driver is calling, Agent is calling, and so on. Validation rule:
  • It must include alphanumeric characters, and its length must not exceed 64 characters.
StringRequired
callOptionsIt is a JSON object with the following properties:
  • receiver_image (string): URL that displays the receiver's image to the initiator of the call (optional).
  • initiator_image (string): URL that displays the initiator's image to the receiver of the call (optional).
JSON ObjectOption

📘

Note

Starting from v0.0.6.2-m2p-beta, the SignedCallAPI.getInstance().call API is deprecated. Instead, use the overloaded version of the call API as mentioned below:

OutgoingCallResponse outgoingCallResponseListener = new OutgoingCallResponse() {
    @Override
    public void onSuccess() {
        //App is notified on the main thread when the call-request is accepted and being processed by the signalling channel
    }

    @Override
    public void onFailure(CallException callException) {
        //App is notified on the main thread when the call is failed
        Log.d("SignedCall: ", "error code: " + callException.getErrorCode()
               + "\n error message: " + callException.getMessage()
               + "\n error explanation: " + callException.getExplanation());

        if (callException.getErrorCode() == CallException.BadNetworkException.getErrorCode()) {
            //Handle this error here
        }
}
  
JSONObject customKeys = new JSONObject();
try {
   customKeys.put("key1", "value1");
   customKeys.put("key2", "value2");
} catch (JSONException e) {
  //no-op
}
  
P2PCallOptions callOptions = new P2PCallOptions.Builder(receiverCuid, callContext)
        .setCustomMetaData(new SCCustomMetaData(
                "<URL that displays the receiver's image to the initiator of the call>",
                "<URL that displays the initiator's image to the receiver of the call>",
              customKeys))
        .build();

SignedCallAPI.getInstance().call(getApplicationContext(), callOptions, outgoingCallResponseListener);
val outgoingCallResponseListener = object : OutgoingCallResponse {
    override fun onSuccess() {
        // App is notified on the main thread when the call-request is accepted and being processed by the signalling channel
    }

    override fun onFailure(callException: CallException) {
        // App is notified on the main thread when the call is failed
        Log.d("SignedCall: ", "error code: ${callException.errorCode}\n" +
                "error message: ${callException.message}\n" +
                "error explanation: ${callException.explanation}")

        if (callException.errorCode == CallException.BadNetworkException.errorCode) {
            // Handle this error here
        }
    }
}

val customKeys = JSONObject()
try {
    customKeys.put("key1", "value1")
    customKeys.put("key2", "value2")
} catch (e: JSONException) {
    // no-op
}

val callOptions = P2PCallOptions.Builder(receiverCuid, callContext)
        .setCustomMetaData(SCCustomMetaData(
                "<URL that displays the receiver's image to the initiator of the call>",
                "<URL that displays the initiator's image to the receiver of the call>",
                customKeys))
        .build()

SignedCallAPI.getInstance().call(applicationContext, callOptions, outgoingCallResponseListener)

To pass metadata for P2P calls, you can use the setCustomMetaData(SCCustomMetaData customMetaData) method available in the P2PCallOptions.Builder class. This method allows you to provide URLs for images displayed to both the initiator and the receiver of the call. Additionally, you can include custom keys to provide more context or information about the call.

The customMetaData parameter has a size limit of 2048 bytes. Ensure that the total size of the data stored in customMetaData parameter does not exceed this limit.

The SDK provides details configured in the P2PCallOptions instance and custom metadata through call-related callback events. Refer to the Handle Call Events section on how to handle call events.

Call Quality Control

The Signed Call Android SDK checks the ping latency before processing the call initiation request. If the latency is more than 3 seconds, the Signed Call Android SDK does not process the call request and returns a CallException.BadNetworkException exception.

Receive a Signed Call

Signed Call uses the following routing channels to receive Signed Calls at the receiver's end:

Socket Channel

It is a primary routing channel. Signed Call Android SDK requires a successful initialization to receive a call on the socket channel.

FCM Channel

It is a secondary or fallback routing channel that is used when the receiver is not connected to the primary routing channel (Socket).

To enable the FCM channel, follow the steps below:

  1. Add your FCM Server Key to the Signed Call section of the CleverTap dashboard. Ignore it if you have already added it.

  2. Add the following code to your Application class:

CleverTapAPI.setSignedCallNotificationHandler(new SignedCallNotificationHandler());
CleverTapAPI.setSignedCallNotificationHandler(SignedCallNotificationHandler())
  1. Add the following code inside your FirebaseMessagingService:
public class MyFcmMessageListenerService extends FirebaseMessagingService {
    @Override
    public void onMessageReceived(RemoteMessage message){
        new CTFcmMessageHandler().createNotification(getApplicationContext(), message);
    }
}
class MyFcmMessageListenerService : FirebaseMessagingService() {

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)
        CTFcmMessageHandler().createNotification(applicationContext, message)
    }
}

📘

Note

If you use CleverTap's Listener Service. to handle the push notifications, then step 3 is not required. However, if you use both, the Signed Call Android SDK receives duplicate pushes for the same VoIP call. The SDK then rejects the duplicate push, which may cause the initiator to experience the receiver as busy on another call, even though the receiver would still be able to pick the call corresponding to initial VoIP push.

📘

Important

The Missed Call Implementation is mandatory to receive calls via FCM channel for Signed Call Android v0.0.2 and below.

Handle Call Events

The approach to handling call events depends on the version of the Signed Call Android SDK you use.

For SDK Versions Below 0.0.6-m2p-beta

📘

Deprecated Event

  • The CALLEE_BUSY_ON_ANOTHER_CALL event is deprecated from versions starting from v0.0.5.5 and below v0.0.6-m2p-beta.
  • It is split into two new events -CALL_DECLINED_DUE_TO_BUSY_ON_VOIP and CALL_DECLINED_DUE_TO_BUSY_ON_PSTN. These events help differentiate calls declined due to another Signed Call(VoIP) or a PSTN call.

If you're using the Signed Call Android SDK version below 0.0.6-m2p-beta, add the following code to register the listener to receive changes in the VoIP call states:

SignedCallAPI.getInstance().registerVoIPCallStatusListener(new SCVoIPCallStatusListener() {
    @Override
    public void callStatus(final SCCallStatusDetails callStatusDetails) {
        //App is notified on the main thread to notify the changes in the call-state
        Log.d(LOG_TAG, "callStatus is invoked with: " + callStatusDetails.toString());

        SCCallStatusDetails.CallDirection direction = callStatusDetails.getDirection();
        VoIPCallStatus callStatus = callStatusDetails.getCallStatus();
        CallDetails callDetails = callStatusDetails.getCallDetails();
        SignallingChannel channel = callDetails.channel;
      
        if (direction.equals(CallDirection.OUTGOING)) {
            //Handle events for initiator of the call

            if (callStatus == VoIPCallStatus.CALL_IS_PLACED) {
                // When the call is successfully placed
            }  else if (callStatus == VoIPCallStatus.CALL_RINGING) {
                // When the call starts ringing on the receiver's device
            } else if (callStatus == VoIPCallStatus.CALL_CANCELLED) {
                // When the call is cancelled from the initiator's end
            } else if (callStatus == VoIPCallStatus.CALL_CANCELLED_DUE_TO_RING_TIMEOUT) {
                // When the call is call is cancelled due to a ring timeout. 
                // This event is reported when the SDK fails to establish communication with the receiver, often due to an offline device or a device with low bandwidth.
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED) {
                // When the call is declined from the receiver's end
            } else if (callStatus == VoIPCallStatus.CALL_MISSED) {
                // When the call is missed at the receiver's end
            } else if (callStatus == VoIPCallStatus.CALL_ANSWERED) {
                // When the call is picked up by the receiver
            } else if (callStatus == VoIPCallStatus.CALL_IN_PROGRESS) {
                // When the connection to the receiver is established
            } else if (callStatus == VoIPCallStatus.CALL_OVER) {
                // When the call has been disconnected
            } else if (callStatus == VoIPCallStatus.CALLEE_BUSY_ON_ANOTHER_CALL) {
                // When the receiver is busy on another call(includes both VoIP or PSTN)
            }  else if (callStatus == VoIPCallStatus.CALL_DECLINED_DUE_TO_BUSY_ON_VOIP) {
                // When the receiver is busy in a VoIP call
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED_DUE_TO_BUSY_ON_PSTN) {
                // When the receiver is busy in a PSTN call
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED_DUE_TO_LOGGED_OUT_CUID) {
                // When the receiver's cuid is logged out and logged in with different cuid  
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED_DUE_TO_NOTIFICATIONS_DISABLED) {
                // When the receiver's Notifications Settings are disabled from application settings
            } else if (callStatus == VoIPCallStatus.CALLEE_MICROPHONE_PERMISSION_NOT_GRANTED) {
                // When the Microphone permission is denied or blocked while receiver answers the call
            } else if (callStatus == VoIPCallStatus.CALLEE_MICROPHONE_PERMISSION_BLOCKED) {
                // When the microphone permission is blocked at the receiver's end.
            } else if (callStatus == VoIPCallStatus.CALL_FAILED_DUE_TO_INTERNAL_ERROR) {
                // When the call fails after signalling. Possible reasons could include low internet connectivity, low RAM available on device, SDK fails to set up the voice channel within the time limit
            }
        } else if (direction.equals(CallDirection.INCOMING)) {
                //Handle events for receiver of the call
                // Receiver will get the same list of events as above so handle accordingly
        }
     }
});
SignedCallAPI.getInstance().registerVoIPCallStatusListener(object : SCVoIPCallStatusListener {
    override fun callStatus(callStatusDetails: SCCallStatusDetails) {
        // App is notified on the main thread to notify the changes in the call-state
        Log.d(LOG_TAG, "callStatus is invoked with: ${callStatusDetails.toString()}")

        val direction = callStatusDetails.direction
        val callStatus: VoIPCallStatus = callStatusDetails.callStatus
        val callDetails: CallDetails = callStatusDetails.callDetails
        val channel: SignallingChannel = callDetails.channel

        if (direction == SCCallStatusDetails.CallDirection.OUTGOING) {
            // Handle the events for initiator of the call

            when (callStatus) {
                VoIPCallStatus.CALL_IS_PLACED -> {
                    // When the call is successfully placed
                }
                VoIPCallStatus.CALL_RINGING -> {
                    // When the call starts ringing on the receiver's device
                }
                VoIPCallStatus.CALL_CANCELLED -> {
                    // When the call is cancelled from the initiator's end
                }
                VoIPCallStatus.CALL_DECLINED -> {
                    // When the call is declined from the receiver's end
                }
                VoIPCallStatus.CALL_MISSED -> {
                    // When the call is missed at the receiver's end
                }
                VoIPCallStatus.CALL_ANSWERED -> {
                    // When the call is picked up by the receiver
                }
                VoIPCallStatus.CALL_IN_PROGRESS -> {
                    // When the connection to the receiver is established
                }
                VoIPCallStatus.CALL_OVER -> {
                    // When the call has been disconnected
                }
                VoIPCallStatus.CALLEE_BUSY_ON_ANOTHER_CALL -> {
                    // When the receiver is busy on another call
                }
                VoIPCallStatus.CALL_DECLINED_DUE_TO_LOGGED_OUT_CUID -> {
                    // When the receiver's cuid is logged out and logged in with a different cuid
                }
                VoIPCallStatus.CALL_DECLINED_DUE_TO_NOTIFICATIONS_DISABLED -> {
                    // When the receiver's Notifications Settings are disabled from application settings
                }
                VoIPCallStatus.CALLEE_MICROPHONE_PERMISSION_NOT_GRANTED -> {
                    // When the Microphone permission is denied or blocked while the receiver answers the call
                }
                VoIPCallStatus.CALLEE_MICROPHONE_PERMISSION_BLOCKED -> {
                    // When the microphone permission is blocked at the receiver's end.
                }
                VoIPCallStatus.CALL_FAILED_DUE_TO_INTERNAL_ERROR -> {
                    // When the call fails after signalling. Possible reasons could include low internet connectivity, low RAM available on device, SDK fails to set up the voice channel within the time limit
                }
            }
        } else if (direction == SCCallStatusDetails.CallDirection.INCOMING) {
            // Handle the events for the receiver of the call
            // Receiver will get the same list of events as above so handle accordingly
           
        }
    }
})

For SDK Versions 0.0.6.2-m2p-beta and above

Starting from v0.0.6.2-m2p-beta, the SDK introduces a breaking change where the SCCallOptions class is provided instead of the CallDetails class. The SCCallOptions class encapsulates the information for both P2P (Person-to-Person) and M2P (Machine-to-Person) calls to handle the callback events for both types of calls.

To implement these changes, register the listener using the following code:

SignedCallAPI.getInstance().registerVoIPCallStatusListener(new SCVoIPCallStatusListener() {
    @Override
    public void callStatus(final SCCallStatusDetails callStatusDetails) {
        //App is notified on the main thread to notify the changes in the call-state
        Log.d(LOG_TAG, "callStatus is invoked with: " + callStatusDetails.toString());

        CallType callType = callStatusDetails.getCallType();
        VoIPCallStatus callStatus = callStatusDetails.getCallStatus();
        SCCallOptions callOptions = callStatusDetails.getCallOptions();
  
         if (CallType.M2P.equals(callType)) {
            M2PCallOptions m2PCallOptions = (M2PCallOptions) callOptions;
            if (callStatus == VoIPCallStatus.DTMF_INPUT_RECEIVED) {
                // When a DTMF input is received from M2P keypad screen
                DTMFInput dtmfInput = m2PCallOptions.getDTMFInput();
                if (dtmfInput != null) {
                    Toast.makeText(this, dtmfInput.getInputKey() + " is pressed!", Toast.LENGTH_SHORT).show();
                }
            } else if (callStatus == VoIPCallStatus.CALL_IS_PLACED) {
                // When the call is successfully placed
            } else if (callStatus == VoIPCallStatus.CALL_CANCELLED) {
                // When the call is cancelled from the initiator's end
            } else if (callStatus == VoIPCallStatus.CALL_CANCELLED_DUE_TO_RING_TIMEOUT) {
                // When the call is call is cancelled due to a ring timeout. 
                // This event is reported when the SDK fails to establish communication with the receiver, often due to an offline device or a device with low bandwidth.
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED) {
                // When the call is declined from the receiver's end
            } else if (callStatus == VoIPCallStatus.CALL_MISSED) {
                // When the call is missed at the receiver's end
            } else if (callStatus == VoIPCallStatus.CALL_ANSWERED) {
                // When the call is picked up by the receiver
            } else if (callStatus == VoIPCallStatus.CALL_IN_PROGRESS) {
                // When the connection to the receiver is established
            } else if (callStatus == VoIPCallStatus.CALL_OVER) {
                // When the call has been disconnected
                List<DTMFInput> dtmfInputList = m2PCallOptions.getDtmfInputList();
                if (dtmfInputList != null) {
                    Toast.makeText(this, "list of DTMF inputs: " + dtmfInputList.size(), Toast.LENGTH_SHORT).show();
                }
            } else if (callStatus == VoIPCallStatus.CALL_CANCELLED_DUE_TO_CAMPAIGN_NOTIFICATION_CANCELLED) {
                // When the M2P call is cancelled by clicking on cancel CTA from campaign's notification
            } else if (callStatus == VoIPCallStatus.CALLEE_BUSY_ON_ANOTHER_CALL) {
                // When the receiver is busy on another call(includes both VoIP or PSTN)
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED_DUE_TO_LOGGED_OUT_CUID) {
                // When the receiver's cuid is logged out and logged in with different cuid  
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED_DUE_TO_NOTIFICATIONS_DISABLED) {
                // When the receiver's Notifications Settings are disabled from application settings
            } else if (callStatus == VoIPCallStatus.CALLEE_MICROPHONE_PERMISSION_NOT_GRANTED) {
                // When the Microphone permission is denied or blocked while receiver answers the call
            }
        } else if (CallType.P2P.equals(callType)) {
            //Handle events for P2P calls
            P2PCallOptions p2PCallOptions = (P2PCallOptions) callOptions;
            Log.d(LOG_TAG, "P2PDetails: " + p2PCallOptions.toString());  
           
            if (callStatus == VoIPCallStatus.CALL_IS_PLACED) {
                // When the call is successfully placed
            } else if (callStatus == VoIPCallStatus.CALL_CANCELLED) {
                // When the call is cancelled from the initiator's end
            } else if (callStatus == VoIPCallStatus.CALL_CANCELLED_DUE_TO_RING_TIMEOUT) {
                // When the call is call is cancelled due to a ring timeout. 
                // This event is reported when the SDK fails to establish communication with the receiver, often due to an offline device or a device with low bandwidth.
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED) {
                // When the call is declined from the receiver's end
            } else if (callStatus == VoIPCallStatus.CALL_MISSED) {
                // When the call is missed at the receiver's end
            } else if (callStatus == VoIPCallStatus.CALL_ANSWERED) {
                // When the call is picked up by the receiver
            } else if (callStatus == VoIPCallStatus.CALL_IN_PROGRESS) {
                // When the connection to the receiver is established
            } else if (callStatus == VoIPCallStatus.CALL_OVER) {
                // When the call has been disconnected
            } else if (callStatus == VoIPCallStatus.CALLEE_BUSY_ON_ANOTHER_CALL) {
                // When the receiver is busy on another call(includes both VoIP or PSTN)
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED_DUE_TO_LOGGED_OUT_CUID) {
                // When the receiver's cuid is logged out and logged in with different cuid  
            } else if (callStatus == VoIPCallStatus.CALL_DECLINED_DUE_TO_NOTIFICATIONS_DISABLED) {
                // When the receiver's Notifications Settings are disabled from application settings
            } else if (callStatus == VoIPCallStatus.CALLEE_MICROPHONE_PERMISSION_NOT_GRANTED) {
                // When the Microphone permission is denied or blocked while receiver answers the call
            }
        }
     }
});
SignedCallAPI.getInstance().registerVoIPCallStatusListener(object : SCVoIPCallStatusListener {
    override fun callStatus(callStatusDetails: SCCallStatusDetails) {
        //App is notified on the main thread to notify the changes in the call-state
        Log.d(LOG_TAG, "callStatus is invoked with: ${callStatusDetails.toString()}")

        val callType: CallType = callStatusDetails.callType
        val callStatus: VoIPCallStatus = callStatusDetails.callStatus
        val callOptions: SCCallOptions = callStatusDetails.callOptions

        if (callType == CallType.M2P) {
            val m2PCallOptions = callOptions as M2PCallOptions
            when (callStatus) {
                VoIPCallStatus.DTMF_INPUT_RECEIVED -> {
                    // When a DTMF input is received from M2P keypad screen
                    val dtmfInput: DTMFInput? = m2PCallOptions.dtmfInput
                    dtmfInput?.let {
                        Toast.makeText(this, "${it.inputKey} is pressed!", Toast.LENGTH_SHORT).show()
                    }
                }
                VoIPCallStatus.CALL_IS_PLACED -> {
                    // When the call is successfully placed
                }
                VoIPCallStatus.CALL_CANCELLED -> {
                    // When the call is cancelled from the initiator's end
                }
                VoIPCallStatus.CALL_CANCELLED_DUE_TO_RING_TIMEOUT -> {
                    // When the call is cancelled due to a ring timeout.
                    // This event is reported when the SDK fails to establish communication with the receiver,
                    // often due to an offline device or a device with low bandwidth.
                }
                VoIPCallStatus.CALL_DECLINED -> {
                    // When the call is declined from the receiver's end
                }
                VoIPCallStatus.CALL_MISSED -> {
                    // When the call is missed at the receiver's end
                }
                VoIPCallStatus.CALL_ANSWERED -> {
                    // When the call is picked up by the receiver
                }
                VoIPCallStatus.CALL_IN_PROGRESS -> {
                    // When the connection to the receiver is established
                }
                VoIPCallStatus.CALL_OVER -> {
                    // When the call has been disconnected
                    val dtmfInputList: List<DTMFInput>? = m2PCallOptions.dtmfInputList
                    dtmfInputList?.let {
                        Toast.makeText(this, "list of DTMF inputs: ${it.size}", Toast.LENGTH_SHORT).show()
                    }
                }
                VoIPCallStatus.CALL_CANCELLED_DUE_TO_CAMPAIGN_NOTIFICATION_CANCELLED -> {
                    // When the M2P call is cancelled by clicking on cancel CTA from campaign's notification
                }
                VoIPCallStatus.CALLEE_BUSY_ON_ANOTHER_CALL -> {
                    // When the receiver is busy on another call (includes both VoIP or PSTN)
                }
                VoIPCallStatus.CALL_DECLINED_DUE_TO_LOGGED_OUT_CUID -> {
                    // When the receiver's CUID is logged out and logged in with different CUID
                }
                VoIPCallStatus.CALL_DECLINED_DUE_TO_NOTIFICATIONS_DISABLED -> {
                    // When the receiver's Notifications Settings are disabled from application settings
                }
                VoIPCallStatus.CALLEE_MICROPHONE_PERMISSION_NOT_GRANTED -> {
                    // When the Microphone permission is denied or blocked while receiver answers the call
                }
            }
        } else if (callType == CallType.P2P) {
            // Handle events for P2P calls
            val p2PCallOptions = callOptions as P2PCallOptions
            Log.d(LOG_TAG, "P2PDetails: ${p2PCallOptions.toString()}")
            
            when (callStatus) {
                VoIPCallStatus.CALL_IS_PLACED -> {
                    // When the call is successfully placed
                }
                VoIPCallStatus.CALL_CANCELLED -> {
                    // When the call is cancelled from the initiator's end
                }
                VoIPCallStatus.CALL_CANCELLED_DUE_TO_RING_TIMEOUT -> {
                    // When the call is cancelled due to a ring timeout.
                    // This event is reported when the SDK fails to establish communication with the receiver,
                    // often due to an offline device or a device with low bandwidth.
                }
                VoIPCallStatus.CALL_DECLINED -> {
                    // When the call is declined from the receiver's end
                }
                VoIPCallStatus.CALL_MISSED -> {
                    // When the call is missed at the receiver's end
                }
                VoIPCallStatus.CALL_ANSWERED -> {
                    // When the call is picked up by the receiver
                }
                VoIPCallStatus.CALL_IN_PROGRESS -> {
                    // When the connection to the receiver is established
                }
                VoIPCallStatus.CALL_OVER -> {
                    // When the call has been disconnected
                }
                VoIPCallStatus.CALL_CANCELLED_DUE_TO_CAMPAIGN_NOTIFICATION_CANCELLED -> {
                    // When the M2P call is cancelled by clicking on cancel CTA from campaign's notification
                }
                VoIPCallStatus.CALLEE_BUSY_ON_ANOTHER_CALL -> {
                    // When the receiver is busy on another call (includes both VoIP or PSTN)
                }
                VoIPCallStatus.CALL_DECLINED_DUE_TO_LOGGED_OUT_CUID -> {
                    // When the receiver's CUID is logged out and logged in with different CUID
                }
                VoIPCallStatus.CALL_DECLINED_DUE_TO_NOTIFICATIONS_DISABLED -> {
                    // When the receiver's Notifications Settings are disabled from application settings
                }
                VoIPCallStatus.CALLEE_MICROPHONE_PERMISSION_NOT_GRANTED -> {
                    // When the Microphone permission is denied or blocked while receiver answers the call
                }
            }
        }
    }
})

Retrieve Current Call State (P2P Feature)

To retrieve the current call state, the Signed Call SDK exposes the getCallState() method via SCCallController class. Refer to the code snippets below to retrieve the current call state:

SCCallController callController = SignedCallAPI.getInstance().getCallController();
if (callController != null) {
    SCCallState callState = callController.getCallState();
    Log.d(LOG_TAG, "call is in " + callState.name() + " state");
}
val callController: SCCallController? = SignedCallAPI.getInstance().callController
callController?.let {
    val callState: SCCallState = callController.callState
    Log.d(LOG_TAG, "call is in ${callState.name} state")
}

Possible call states returned by the SDK includeOUTGOING_CALL, INCOMING_CALL, ONGOING_CALL, CLEANUP_CALL and NO_CALL. The NO_CALL state is returned when there is no ongoing active call.

Return to Active Call (P2P Feature)

To return to an active call, the Signed Call SDK exposes the getBackToCall(context) method via SCCallController class. Refer to the code snippets below to return to an active call:

SCCallController callController = SignedCallAPI.getInstance().getCallController();
if (callController != null && callController.getCallState() != SCCallState.NO_CALL) {
    boolean result = callController.getBackToCall(getApplicationContext());
    Log.d(LOG_TAG, "result: " + result);
}
val callController = SignedCallAPI.getInstance().callController
callController?.let { controller ->
    if (controller.callState != SCCallState.NO_CALL) {
        val result = controller.getBackToCall(applicationContext)
        Log.d(LOG_TAG, "result: $result")
    }
}

To navigate the user to the active call, call the getBackToCall(context) method if an active call exists. The Signed Call Android SDK logs an error message if no call is in progress. The SDK returns true if the call screen is successfully relaunched. Otherwise, it returns false in case of an error or there is no call in progress.

Close Socket Connection (P2P Feature)

The Signed Call Android SDK uses the socket connection to signal VoIP calls from the initiator to the receiver. After successful SDK initialization, the socket connection to initiate or receive VoIP calls opens.

If the socket is left open for a longer period, the application might show a system-generated notification to the user that the application is draining the device's battery. Therefore, we recommend disconnecting the socket after all expected/pending transactions are over.

To close the socket connection, use the following method as per your business use case:

SignedCallAPI.getInstance().disconnectSignallingSocket(getApplicationContext());
SignedCallAPI.getInstance().disconnectSignallingSocket(applicationContext);

The following is the Signed Call Android SDK behavior after the SDK calls the disconnectSignallingSocket(appContext) method:

FunctionalitySigned Call Android SDK Behavior
Initiate a callUsers cannot initiate calls. If they attempt a VoIP call request, Signed Call Android SDK returns CallException.SignallingSocketConnectionRequiredException within the onFailure(callException) method. It is essential to reinitialize the Signed Call SDK if a use case arises where a call needs to be initiated.
For example, if a user happens to place an order in the application and you want to give the option to initiate a VoIP call, the Signed Call Android SDK needs to be reinitialized.
Receive a callUsers can still receive calls as Signed Call uses FCM as a fallback channel to receive the calls.

Busy Handling

Signed Call Android SDK smartly handles the scenarios when the user is busy on a call.
The following scenarios describe the Signed Call Android SDK behavior:

Scenario 1: The user is busy on a call (VoIP or PSTN), and another user initiates a VoIP call to the busy user. In this case, the Signed Call Android SDK displays User is busy on another call on the outgoing call screen and declines the initiated call.

Scenario 2: The user is busy on a VoIP call, and meanwhile, the user answers a PSTN call meaning that two calls (VoIP and PSTN) are connected simultaneously. In this case, the Signed Call Android SDK prioritizes the PSTN call over the VoIP call by putting the VoIP call on hold for both the receiver and initiator of the VoIP call. After the PSTN call ends, the Signed Call Android SDK resumes the VoIP call.

📘

Prerequisites of Busy Handling Scenarios

The Signed Call Android SDK needs Read Phone State permission to handle the busy handling scenarios of PSTN calls.

  • In scenario 1, Signed Call Android SDK uses this permission to determine if the user is available or busy on a PSTN call. This permission is required for Android 12 and onwards only.
  • In scenario 2, the Signed Call Android SDK supports the underlying implementation only if the user has granted the Read Phone State permission.

Missed Call Solution

If the receiver misses a call, the Signed Call Android SDK shows a missed call notification to the receiver. The Signed Call Android SDK uses action buttons on the missed call notification to display a Call to Action (CTA).

To configure the CTA on the missed call notification, perform the following steps during the initialization of the Signed Call Android SDK:

  1. Create a list of CTAs using the MissedCallAction class.
List<MissedCallAction> missedCallActionsList = new ArrayList<>();
missedCallActionsList.add(new MissedCallAction("<Unique Identifier>", "<label on action-button>"));
val missedCallActionsList: MutableList<MissedCallAction> = ArrayList()
missedCallActionsList.add(MissedCallAction("<Unique Identifier>", "<label on action-button>"))

📘

Action Buttons

You can configure a maximum of three action buttons on a missed call notification.

  1. Handle the click events of the missed call CTAs. To do so, create a custom MissedCallActionsHandler class by implementing the MissedCallNotificationOpenedHandler.
public class MissedCallActionsHandler implements MissedCallNotificationOpenedHandler {

    @Override
    public void onMissedCallNotificationOpened(Context context, MissedCallNotificationOpenResult result) {
        //get the the action-details from result object and handle accordingly
        Log.d(LOG_TAG, "actionId: " + result.action.actionID
                    + ", actionLabel: " + result.action.actionLabel
                    + ", context of call: " + result.callDetails.callContext
                    + ", cuid of caller: " + result.callDetails.callerCuid
                    + ", cuid of callee: " + result.callDetails.calleeCuid
                    + ", initiator's image: " + result.callDetails.initiatorImage
                    + ", receiver's image: " + result.callDetails.receiverImage);
    }
}
class MissedCallActionsHandler : MissedCallNotificationOpenedHandler {

    override fun onMissedCallNotificationOpened(context: Context, result: MissedCallNotificationOpenResult) {
        //get the the action-details from result object and handle accordingly
        Log.d(TAG, "actionId: " + result.action.actionID
                    + " actionLabel: " + result.action.actionLabel
                    + " context of call: " + result.callDetails.callContext
                    + " cuid of caller: " + result.callDetails.callerCuid
                    + " cuid of callee: " + result.callDetails.calleeCuid);
    }
}

Starting SDK version 0.0.6.2-m2p-beta, the Signed Call Android SDK provides an instance of SCCallOptions class for distinct handling of missed call click events for M2P and P2P calls, respectively.

public class MissedCallActionsHandler implements MissedCallNotificationOpenedHandler {

    @Override
    public void onMissedCallNotificationOpened(Context context, MissedCallNotificationOpenResult result) {
        CallType callType = result.getCallType();
        SCCallOptions callOptions = result.getCallOptions();

        if (CallType.P2P.equals(callType)) {
            P2PCallOptions p2PCallOptions = (P2PCallOptions) callOptions;
            Log.d(LOG_TAG, "actionID: " + result.getAction().getActionID()
                    + ", actionLabel: " + result.getAction().getActionLabel()
                    + ", context of call: " + p2PCallOptions.getCallContext()
                    + ", initiator's cuid: " + p2PCallOptions.getInitiatorCuid()
                    + ", receiver's cuid: " + p2PCallOptions.getReceiverCuid()
                    + ", customMetaData: " + (p2PCallOptions.getCustomMetaData() != null
                        ? p2PCallOptions.getCustomMetaData().toJson() : null));
        } else if (CallType.M2P.equals(callType)) {
            M2PCallOptions m2PCallOptions = (M2PCallOptions) callOptions;
            Log.d(LOG_TAG, "actionID: " + result.getAction().getActionID()
                    + ", actionLabel: " + result.getAction().getActionLabel()
                    + ", campaignLabelList: " + m2PCallOptions.getCampaignLabelList()
                    + ", context of call: " + m2PCallOptions.getCallContext());
        }
    }
}
class MissedCallActionsHandler : MissedCallNotificationOpenedHandler {

    override fun onMissedCallNotificationOpened(context: Context, result: MissedCallNotificationOpenResult) {
        val callType = result.callType
        val callOptions = result.callOptions

        if (callType == CallType.P2P) {
            val p2PCallOptions = callOptions as P2PCallOptions
            Log.d(LOG_TAG, "actionID: ${result.action.actionID}," +
                    " actionLabel: ${result.action.actionLabel}," +
                    " context of call: ${p2PCallOptions.callContext}," +
                    " initiator's cuid: ${p2PCallOptions.initiatorCuid}," +
                    " receiver's cuid: ${p2PCallOptions.receiverCuid}," +
                    " customMetaData: ${p2PCallOptions.customMetaData?.toJson()}")
        } else if (callType == CallType.M2P) {
            val m2PCallOptions = callOptions as M2PCallOptions
            Log.d(LOG_TAG, "actionID: ${result.action.actionID}," +
                    " actionLabel: ${result.action.actionLabel}," +
                    " campaignLabelList: ${m2PCallOptions.campaignLabelList}," +
                    " context of call: ${m2PCallOptions.callContext}")
        }
    }
}

📘

Note

The MissedCallActionsHandler must not be a singleton class.

  1. Pass the list of MissedCallAction and the canonical path of the MissedCallActionsHandler in the
    setMissedCallActions(List<MissedCallAction> list, String path) method of the SignedCallInitConfiguration.Builder class.
List<MissedCallAction> missedCallActionsList = new ArrayList<>();
missedCallActionsList.add(new MissedCallAction("<Unique Identifier>", "<label on action-button>"));

//gets the name of the class including its package
String missedCallHandlerPath = MissedCallActionsHandler.class.getCanonicalName();

SignedCallInitConfiguration initConfiguration = new SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
       .setMissedCallActions(missedCallActionsList, missedCallHandlerPath)
       .build();

SignedCallAPI.getInstance().init(getApplicationContext(), initConfiguration, cleverTapAPI, signedCallInitListener);
val missedCallActionsList: MutableList<MissedCallAction> = ArrayList()
missedCallActionsList.add(MissedCallAction("<Unique Identifier>", "<label on action-button>"))

//gets the name of the class including its package
val missedCallHandlerPath = MissedCallActionsHandler::class.java.canonicalName

val initConfiguration = SignedCallInitConfiguration.Builder(options, allowPersistSocketConnection)
    .setMissedCallActions(missedCallActionsList, missedCallHandlerPath)
    .build()

SignedCallAPI.getInstance().init(applicationContext, initConfiguration, cleverTapAPI, signedCallInitListener)

Configure Swipe Off Behaviour (P2P Feature)

By default, the Signed Call SDK terminates a call when the user swipes off the call screen. However, if your app runs a foreground service to maintain operation even after the app's swipe-off, you can modify this default behavior. The SDK allows you to override the default behavior and persist the call within the foreground service managed by your application.

To change the swipe-off behavior, the Signed Call Android SDK exposes a setSwipeOffBehaviourInForegroundService(SCSwipeOffBehaviour) method via the SignedCallInitConfiguration.Builder class. Pass the enum constant SCSwipeOffBehaviour.PERSIST_CALL to ensure the call remains persistent in a foreground service upon swipe off.

SignedCallInitConfiguration initConfiguration = new SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
         .setSwipeOffBehaviourInForegroundService(<pass SCSwipeOffBehaviour constant here>)
         .build();

SignedCallAPI.getInstance().init(getApplicationContext(), initConfiguration, cleverTapAPI, signedCallInitListener)
val initConfiguration = SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
        .setSwipeOffBehaviourInForegroundService(<pass SCSwipeOffBehaviour constant here>)
        .build()

SignedCallAPI.getInstance().init(applicationContext, initConfiguration, cleverTapAPI, signedCallInitListener)

📘

Note

  • To persist a call after the app is swiped off, the app must continue running as a foreground service. If it doesn't, the call will end because there will be no active process to maintain it.
  • For details on ensuring users can still access the call screen after the app is swiped off, refer to the Return to the Active Call section. It is highly recommended to implement these instructions if you are altering the default behavior.

Call Hangup Functionality

The call hangup functionality is user-driven, and the user's decision to end the call depends on it. For example, if one of the users in a call clicks the call hangup button from the ongoing call screen, the Signed Call Android SDK internally manages the call hangup functionality to end the call.

In the case of a metered call, when a business wants to end the call after a specific duration, you must maintain a timer in the application and use the following method to terminate the call when the timer ends:

SCCallController callController = SignedCallAPI.getInstance().getCallController();
if (callController != null) {
    callController.endCall(); 
}
SignedCallAPI.getInstance().callController?.endCall()

Set Local Branding for Call Screen

For Person-to-Person (P2P) calls, you can set the branding from the dashboard or set it up locally.

For a Machine-to-Person (M2P) campaign, it is mandatory to set up the branding locally in your application.

To set up the branding locally, use the overrideDefaultBranding(SignedCallScreenBranding branding) method exposed via the SignedCallInitConfiguration.Builder class to set up the branding:

Refer to the code snippets below to set up the call screen branding.

SignedCallScreenBranding callScreenBranding = new SignedCallScreenBranding(bgColor, fontColor, logoUrl, buttonTheme);
callScreenBranding.setShowPoweredBySignedCall(<true/false>); //set false to hide the label from VoIP call screens. Default value is true. 

SignedCallInitConfiguration initConfiguration = new SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)  
      .overrideDefaultBranding(callScreenBranding)
      .build();

SignedCallAPI.getInstance().init(getApplicationContext(), initConfiguration, cleverTapAPI, signedCallInitListener);
val callScreenBranding = SignedCallScreenBranding(bgColor, fontColor, logoUrl, buttonTheme)

val initConfiguration = SignedCallInitConfiguration.Builder(initOptions, allowPersistSocketConnection)
    .overrideDefaultBranding(callScreenBranding)
    .build()

SignedCallAPI.getInstance().init(applicationContext, initConfiguration, cleverTapAPI, signedCallInitListener)

The parameters to override the dashboard's call screen branding are as follows:

ParameterDescriptionTypeNotes
bgColorThe background color of the call screens.
Note: Use any Hex color code. For example, #000000
StringRequired
fontColorThe color of the text displayed on the call screens.
Note: Use any Hex color code. For example, #ffffff
StringRequired
logoUrlThe image URL that renders on the call screens.
Note: Use an HTTPS URL only.
StringRequired
buttonThemeThe theme of the control buttons shown on the ongoing call screen (Mute, Speaker, and Bluetooth).
Note: The Light theme represents the white color of the buttons whereas Dark is for the black color.
SignedCallScreenBranding.ButtonTheme.LIGHT
OR SignedCallScreenBranding.ButtonTheme.DARK
Required

Debugging

Signed Call Android SDK logs are, by default, set to the SignedCallAPI.LogLevel.INFO level. We recommend you set the Signed Call Android SDK to VERBOSE mode to log warnings or other important messages to the Android logging system during development. To do so, set the debug level to SignedCallAPI.LogLevel.VERBOSE. If you want to disable the Signed Call Android SDK logs for the production environment, you can set the debug level to SignedCallAPI.LogLevel.OFF.

To debug your application with the Signed Call Android SDK:

  1. Set the debug level for the Signed Call Android SDK.
SignedCallAPI.setDebugLevel(SignedCallAPI.LogLevel.INFO);    //Default Log level

SignedCallAPI.setDebugLevel(SignedCallAPI.LogLevel.DEBUG);   //Set Log level to DEBUG log warnings or other important messages

SignedCallAPI.setDebugLevel(SignedCallAPI.LogLevel.VERBOSE); //Set Log level to VERBOSE

SignedCallAPI.setDebugLevel(SignedCallAPI.LogLevel.OFF);     //Switch off the logs for Production environment
SignedCallAPI.setDebugLevel(SignedCallAPI.LogLevel.INFO)    //Default Log level

SignedCallAPI.setDebugLevel(SignedCallAPI.LogLevel.DEBUG)   //Set Log level to DEBUG log warnings or other important messages

SignedCallAPI.setDebugLevel(SignedCallAPI.LogLevel.VERBOSE) //Set Log level to VERBOSE

SignedCallAPI.setDebugLevel(SignedCallAPI.LogLevel.OFF)     //Switch off the logs for Production environment
  1. After setting the debug level to SignedCallAPI.LogLevel.VERBOSE, search for
    [CT]:[SignedCall]. The logcat window displays the handshakes between the Signed Call Android SDK and your application.

Error Handling

The Signed Call Android SDK provides error reporting and handling. The onFailure(InitException) of the SignedCallInitResponse reports all the initialization related errors, where the onFailure(CallException) of the OutgoingCallResponse reports all the Call related errors.

Initialization Errors

Following is the list of the error objects that you may receive when initializing the Signed Call Android SDK:

Error ObjectError CodeError Description
NoInternetException1000No internet connection.
AppContextRequiredException2000The application context is missing.
CleverTapApiInstanceRequiredException2001The CleverTapApi instance is missing.
InitConfigRequiredException2002The initOptions is missing.
SdkNotInitializedException2003The Signed Call Android SDK is not initialized.
MissingAccountIdOrApiKeyException2004The accountId and apiKey parameters are missing.
MissingCuIdException2005The cuid is missing.
InvalidCuidLengthException2006The cuid length is invalid.
InvalidCuidException2007Invalid cuid due to violation of valid cuid rules.
InvalidNameLengthException2008The length of the name parameter is invalid.
InvalidAppIdException2009The appId is invalid.
InvalidBrandingConfigException2010The branding configuration is invalid.
BadRequestException2011The values in initOptions are invalid.
AuthFailureException2012The user authentication is not successful.
NotificationPermissionRequiredException2013The notification permission was not given during the SDK initialization.

Call Errors

Following is the list of possible error objects when making a call:

Error ObjectError CodeError Description
NoInternetException1000No internet connection.
MicrophonePermissionNotGrantedException5001Microphone permission is not available.
InternetLostAtReceiverEndException5002The Internet is lost at the receiver's end before the call connects.
ContactNotReachableException5003The receiver is unreachable.
BadNetworkException5004The Signed Call Android SDK can not initiate the call because of a poor network.
CanNotCallSelfException5005The Receiver and Initiator's cuid is the same.
CallContextRequiredException5006The context of the call is missing.
CallContextExceededBy64Exception5007The length of the context message exceeds the limit of 64 characters.
InvalidAppContextException5008Invalid context of the application.
CalleeInfoRequiredException5009The receiver's cuid is missing.
VoIPCallException5010The signed call can not be initiated to the unregistered/invalid cuid.
SignallingSocketConnectionRequiredException5011The socket required to initiate a call is not connected to the signaling channel.
IncorrectParamsInCallOptionsException5012The callOptions parameters are invalid.
CanNotProcessCallRequest5013Cannot process new call requests as the Signed Call Android SDK is already processing another.
CallFeatureNotAvailable5014The call feature is not enabled to initiate the call.

FAQs

Q. Is Signed Call accountId and apiKey the same as CleverTap's accountId and token?

A. No. Signed Call accountId and apiKey differ from CleverTap's accountId and token. You can find these details under your dashboard's Signed Call Settings.

Q. Does the Signed Call Android SDK support In-App calls over Bluetooth?

A. Yes. The Signed Call Android SDK has built-in Bluetooth support. It requires a runtime BLUETOOTH_CONNECT permission for Android 12 and onwards.

Q. What channels are used for call routing by Signed Call Android SDK?

A. Signed Call Android SDK uses an active socket connection as soon as the SDK is initialized. The socket connection is a primary routing channel to receive the calls, whereas FCM is a fallback channel in case the receiver is not connected to the socket channel. This socket connection processes the call requests raised to make a call. To know more, refer to the Best practices for initializing Signed Call SDKs.