Getting Started with Android libSoftphone

This section describes the steps to create a simple Android application using Acrobits libSoftphone SDK.

Setting up libSoftphone SDK

Ensure the Android Studio installation is complete. Then, to add libSoftphone SDK to the Android Gradle project:

  1. Locate and open the project-level build.gradle file in the root directory of your Android project.

  2. Add Acrobits Maven repository in the repository block and include your application identifier and license key:

    1maven {
    2    url "https://maven.acrobits.net/repository/maven-releases/"
    3    credentials {
    4        username <APPLICATION_IDENTIFIER>
    5        password <LICENSE_KEY>
    6    }
    7}
    

    Note

    • The license key - <LICENSE_KEY> - can be obtained from the portal after completing step 1 in the Quick Start Guide.

    • We recommend using dependencyResolutionManagement to add the Maven repository.

  3. Add the libSoftphone dependency for the SDK in the application-level build.gradle file:

    1dependencies {
    2    implementation 'cz.acrobits:libsoftphone:<version>'
    3}
    

    Note

    Replace <version> with the version of the SDK.

Java 8 Requirement

The SDK is largely marked with the @FunctionalInterface annotation. In such cases, Acrobits highly recommends using the Java 8 lambda syntax.

This documentation assumes you are using Java 8 or a higher version of the language.

Note

We highly recommend include Java 8 support to the build.gradle, but this is not strictly required to do so. For more information, go to Android Core library desugaring at developer.android.com/studio/write/java8-support.

Dependencies List

libSoftphone SDK doesn’t introduce any transitive dependencies to your project.

The compile-time and runtime dependencies can be found by inspecting the artifacts Project Object Model (POM) file.

Initializing the SDK

To initialize libSoftphone before using the SDK functionality provided by this, use cz.acrobits.libsoftphone.Instance. This instance is the entry point to control libSoftphone SDK. If you are not using the tools provided by the SDK before initializing the instance, go to step 2 to begin the initialization.

To initialize libSoftphone SDK:

  1. Load the libSoftphone SDK library in your Android project by invoking the Instance.loadLibrary method.

    Important

    You must perform step 1 if you need to use other classes from the SDK or the SDK’s tools before initializing the SDK instance. For example, if provisioning the instance if required during initialization, you must load the SDK library beforehand.

    • The library needs a valid context to be loaded successfully. The context typically refers to your activity or application instance, depending on the specific scenario in which the library is being loaded. For example, to load a library in the onCreate method, use the following code:

      1@Override
      2void onCreate(@Nullable Bundle savedState)
      3{
      4    Instance.loadLibrary(this);
      5    super.onCreate(savedState);
      6}
      
    • After the library is loaded, you can use classes from the cz.acrobits.ali package.

    • Classes from support package cz.acrobits.ali.support may be used even without loading the library.

  1. To initialize the SDK instance, use Instance.init.

    • This step loads the SDK library automatically.

    • Use different overload versions of Instance.init method (cz.acrobits.libsoftphone.Instance.init) based on the specific requirements of the application. To illustrate, consider using the following examples:

      • For the Demo SDK, initialize the onCreate method with the following code:

        1@Override
        2void onCreate(@Nullable Bundle savedState)
        3{
        4    Instance.init(this);
        5    //Instance.setObserver(sListeners); // to observe events
        6    super.onCreate(savedState);
        7}
        
      • For the libSoftphone SDK, where instance initialization requires a SaaS identifier, load the library, preparing the SaaS provisioning, and then initialize the library with the following code:

         1@Override
         2void onCreate(@Nullable Bundle savedState)
         3{
         4    Instance.loadLibrary(this);
         5    Xml provisioning = new Xml("provisioning");
         6    Xml saas = new Xml("saas");
         7    saas.replaceChild("identifier", SAAS_IDENTIFIER);
         8    provisioning.replaceChild(saas);
         9    Instance.init(this, provisioning);
        10    //Instance.setObserver(sListeners); // to observe events
        11    super.onCreate(savedState);
        12}
        
      • Another overload version of Instance.init allows you to pass the preference class (cz.acrobits.libsoftphone.Preferences) or factory parameter.

        After the initialization, libSoftphone SDK loads stored preferences and accounts. Depending on the configuration and application state, the SDK then starts registering to SIP servers and listening for incoming calls.

  2. Set an observer (cz.acrobits.libsoftphone.Observer) to receive notifications when certain events occur in the SDK. See Observing Events from SDK for more information.

Reporting Application States

libSoftphone SDK must have accurate information about the current state of the application for proper handling of SIP registration, push calls, and overall SDK functionality. Acrobits recommends using the cz.acrobits.support.lifecycle.LifecycleTracker class. Once initialized, this class starts reporting the latest application state automatically.

The LifecycleTracker Class

In the underlying structure, the LifecycleTracker uses the registered and custom implementation of android.app.Application.ActivityLifecycleCallbacks to track the lifecycle event of activities occur in the application.

When activities transition between states after the SDK is already initialized, the LifecycleTracker retrieves the current state of the SDK Instance.State and reports it to the SDK.

Using the LifecycleTracker Class

To initialize the LifecycleTracker class so that it begins reporting the application state as soon the SDK is initialized:

1<provider android:authorities="${applicationId}.libsoftphone.lifecycle"
2    android:name="cz.acrobits.libsoftphone.support.lifecycle.LifecycleTrackerInitializer"
3    android:exported="false">
4</provider>

The cz.acrobits.libsoftphone.support.lifecycle.LifecycleTrackerInitializer handles the initialization of the lifecycle tracker for you.

Note

To manually initialize this class, include the cz.acrobits.support.lifecycle.LifecycleTracker.init line before calling cz.acrobits.libsoftphone.Instance.init.

Manually Reporting Application States

To manually report application state, use the Instance.State.update() method. You should supply the Instance.State state value for your current top-most android.app.Activity.

Managing SIP Account

SIP accounts are specified in XML format.

To view all the recognized XML nodes and values that can be used to configure SIP accounts, go to the Account XML documentation at doc.acrobits.net/cloudsoftphone/account.html#account-xml.

Creating a SIP Account

Include the following code to create an account with basic configuration containing the username, password, and SIP domain:

1Xml account = new Xml("account");
2account.setAttribute(Account.Attributes.ID, "Test Account");
3account.setChildValue(Account.USERNAME, userName);
4account.setChildValue(Account.PASSWORD, password);
5account.setChildValue(Account.HOST, domain);
6Instance.Registration.saveAccount(new AccountXml(account, MergeableNodeAttributes.gui()));

Note the following:

  • An Account.Attributes.ID attribute must be specified to identify an account. If not, a unique one is generated for that account upon calling Instance.Registration.saveAccount().

  • Calling Instance.Registration.saveAccount() with an XML that has the same Account.Attributes.ID as an existing account replaces the existing account with the new values.

  • If you save a new account with an Account.Attributes.ID that matches the ID of an existing account but different account configuration, the new account (re)registers asynchronously.

Enabling or Disabling a SIP Account

Use AccountXml.setEnabled(boolean enabled) to enable or disable a SIP account, and then call Instance.Registration.saveAccount() to apply changes:

1AccountXml accountXml = Instance.Registration.getAccount("Test Account").clone();
2accountXml.setEnabled(true);
3Instance.Registration.saveAccount(accountXml);

Default SIP Account

Every application should have a default account.

Default application account:

  • After disabling the default application account, set a new one.

  • If the application uses an account only, call the Instance.Registration.setDefaultAccount(null) method to set it as the default.

  • If the application supports multiple accounts, call the Instance.Registration.setDefaultAccount(accountId) method and include a specific ID to set that account as the default.

Updating Account Configuration

To change the configuration of an account:

  1. Call the Instance.Registration.getAccount() method to obtain a reference to the AccountXml of the account you want to modify.

  2. Before making changes, call the AccountXml.clone() method to obtain a mutable reference to the account.

  3. Use appropriate methods or properties of the AccountXml object to make changes on the account configuration.

  4. Call the Instance.Registration.saveAccount() method to save changes.

The following code snippet illustrates changing the incoming call mode to push for the Test Account:

1AccountXml accountXml = Instance.Registration.getAccount("Test Account").clone();
2accountXml.mergeValue("icm", "push", MergeableNodeAttributes.gui());
3Instance.Registration.saveAccount(accountXml);

Deleting an Account

Call the Instance.Registration.deleteAccount(accountId) method to delete accounts.

If you delete a default application account, you need set a new one. See the Default SIP Account section for the methods to set a default account.

Observing Events from SDK

Set an observer to receive notifications when desired events occur in the SDK.

Setting an Observer

Set an observer immediately after calling the cz.acrobits.libsoftphone.Instance.init() method. See the Initializing the SDK section for the details to initialize the SDK.

To observe events from the SDK, we recommend deriving a class from the cz.acrobits.support.lifecycle.Listeners class:

 1public static final Listeners sListeners = new Listeners()
 2{
 3    //******************************************************************
 4    @Override
 5    public @Nullable Object getRingtone(@NonNull CallEvent call)
 6    //******************************************************************
 7    {
 8        return RingtoneManager.getRingtone(context, Settings.System.DEFAULT_RINGTONE_URI);
 9    }
10};

Once you have an instance of your derived cz.acrobits.support.lifecycle.Listeners class, set it as the observer by calling Instance.setObserver(sListeners):

1// Instance.init() ...
2Instance.setObserver(sListeners);

Registering Event Listeners

Call the cz.acrobits.support.lifecycle.Listeners.register() method to register event listeners.

The cz.acrobits.support.lifecycle.Listeners is not lifecycle-aware and uses hard-references. Therefore, you must unregister your listeners manually by using the cz.acrobits.support.lifecycle.Listeners.unregister() method .

Placing a Call

Follow the steps in this section to use the SIP calling function provided by libSoftphone SDK.

  1. Create a StreamParty for the phone number to which you want to place a call.

    1StreamParty party = new StreamParty(phoneNumber);
    2party.match(Instance.Registration.getDefaultAccountId()); // match number in contacts and normalize StreamParty fields
    
  2. Create a CallEvent using the StreamParty, set the account from which the call should be placed, and set your desired dial action.

    1CallEvent event = new CallEvent(party.toRemoteUser());
    2event.setAccount(Instance.Registration.getDefaultAccountId());
    3event.transients.put(CallEvent.Transients.DIAL_ACTION, DialAction.VOICE_CALL.id);
    
  3. Place a call. The call is considered successful when Instance.Events.post() returns Instance.Events.PostResult.SUCCESS.

    1int result = Instance.Events.post(event);
    2if(res == Instance.Events.PostResult.SUCCESS)
    3{
    4    Toast.makeToast(context, "Call placed succesfully", Toast.LENGTH_SHORT).show();
    5}
    6else
    7{
    8    Toast.makeToast(context, String.format("Call failed: %d", res), Toast.LENGTH_SHORT).show();
    9}
    

Managing Preferences

libSoftphone SDK provides the app-specific preferences function that uses to set up and manage the Preferences menu items in the application.

Initializing a Preference Key

Two ways to create and initialize a preference key in a class that extends the Preferences class:

  • Add the code snippet final Key<Boolean> trafficLogging = new Key<>("sipTrafficLogging"), or

  • Add the code snippet final Preferences.Key<Boolean> trafficLogging = new Preferences().new Key<>("sipTrafficLogging") in anywhere of your code.

Reading Preference Key Values

Use the get() method to retrieve the current value of a read-write Preferences.Key or a read-only Preferences.ROKey.

For example, to check if SIP traffic logging is enabled, use the following codes:

1if (Instance.preferences.trafficLogging.get())
2{
3    Toast.makeText(context, "Logging is enabled", Toast.LENGTH_SHORT).show();
4}

Changing Preference Key Values

Use the set() method to change the value of a read-write Preferences.Key.

For instance, to enable SIP traffic logging, use the following codes:

1Instance.preferences.trafficLogging.set(true);

Resetting a Preference Key

Use the Preferences.Key.reset() method to reset a preference key to the default value.

Detecting Changes in Preference-Key Values

To detect changes in preference key values, attach a listener by implementing the OnSettingsChanged interface:

 1class SettingsListener implements OnSettingsChanged
 2{
 3    @Override
 4    public void onSettingsChanged()
 5    {
 6        if (Instance.preferences.trafficLogging.hasChanged())
 7        {
 8            Toast.makeText(context, String.format("Logging is now set to %b", Instance.preferences.trafficLogging.get()), Toast.LENGTH_SHORT).show();
 9        }
10    }
11}
12
13// ...
14
15sListeners.register(new SettingsListener());

The OnSettingsChanged.onSettingsChanged method is also invoked whenever changes are made to SIP account configuration.

Using Call Event History

Use the Call Event History feature to store call history in the device’s local storage and to display a list of recent calls in your app. This feature is disabled by default.

  1. To enable the Call Event History feature, add the following code in the app’s provisioning XML file:

    1<root>
    2    <saas>
    3        <identifier>your license key</identifier>
    4    </saas>
    5    <prefKeys>
    6        <prop name="historyStorageType" default="empty"/>
    7    </prefKeys>
    8</root>
    

    You can refer to the Initializing the SDK section to review the steps for supplying the provisioning file during SDK initialization.

  2. Add the Call Event History listeners. Refer to the Setting an Observer section to set the Java or Kotlin observer for the SDK.

    The following code snippet shows how to register an onEventsChanged listener for Android:

     1Listeners listeners = new Listeners()
     2{
     3    @Nullable
     4    @Override
     5    public Object getRingtone(@NonNull CallEvent callEvent)
     6    {
     7        return null;
     8    }
     9};
    10// Instance.setObserver(sListeners) ...
    11
    12listeners.register((Listeners.OnEventsChanged) (changedEvents, changedStreams) -> {
    13    // From here, you can trigger a refresh of the call history list in your app
    14
    15    // get changed events - only set if changedEvents.many is false
    16    long[] changedIds = changedEvents.eventIds; // id's of changed events
    17    // check if many events changed, if true, changedIds is null
    18    boolean manyChanged = changedEvents.many;
    19    // check if any call event changed
    20    boolean callEventsChanged = changedStreams.streamKeys != null && Arrays.stream(changedStreams.streamKeys)
    21                                                                                .collect(Collectors.toList())
    22                                                                                .contains(StreamQuery.legacyCallHistoryStreamKey());
    23});
    24
    25// Register onMissingCalls listener
    26listeners.register((Listeners.OnMissedCalls) callEvents -> {
    27    // triggers in case of any new missing call entry(s) appears in history)
    28
    29    List<CallEvent> missingCallsUpdate = Arrays.stream(callEvents)
    30                                                .collect(Collectors.toList()); // new missing calls detected (in most cases is one call)
    31
    32    // this can be used for display missing call notification for example
    33});
    34
    35// unregister listener later when not in use anymore
    36// listeners.unregister(...);
    37
    38Instance.setObserver(listeners);
    
  3. To display the call history in your app, perform a query to the device’s local storage and fetch all call events according to your filter and paging requirements, as shown in the following code snipet:

     1CallEventQuery query = new CallEventQuery(); // empty query for calls
     2query.streamKey = StreamQuery.legacyCallHistoryStreamKey(); // set query only calls
     3// apply a filter if needed, here we are filtering only missed calls
     4query.resultMask = CallEvent.Result.MISSED; // set query only missing calls
     5
     6// optional paging for query results
     7EventPaging paging = new EventPaging();
     8/*paging.after = ...;
     9paging.before = ...;
    10paging.olderThan = ...;
    11paging.offset = ...;
    12paging.limit = ...;
    13etc...*/
    14
    15// fetch the call events
    16EventFetchResult result = Instance.Events.fetch(query, paging);
    17
    18// get missed calls count
    19int totalMissingCalls = result.totalCount;
    20// get missed calls list
    21List<CallEvent> missingCalls = Arrays.stream(result.events)
    22                                     .map(CallEvent.class::cast)
    23                                     .collect(Collectors.toList());
    
  4. Use the simple onMissedCalls listener to show notifications for missed calls in your app. This listener is triggered when a new missed call is detected, including those reported to the app via Firebase push notifications, even when the app is not running.

    The following code snippet shows how to register the onMissedCalls listener and show a notification for each new missed call on Android:

     1listeners.register((Listeners.OnMissedCalls) callEvents -> {
     2    // triggers in case of any new missing call entry(s) appears in history
     3
     4    List<CallEvent> missingCallsUpdate = Arrays.stream(callEvents)
     5                                                .collect(Collectors.toList()); // new missing calls detected (in most cases is one call)
     6
     7    // this can be used for display missing call notification for example
     8    CallEventQuery missingCallsQuery = new CallEventQuery(); // Query for total count of missing calls
     9    query.streamKey = StreamQuery.legacyCallHistoryStreamKey();
    10    query.resultMask = CallEvent.Result.MISSED;
    11
    12    EventPaging missingCallsPaging = new EventPaging();
    13    missingCallsPaging.limit = 1; // we need only count so can limit page to one item
    14
    15    int missingCallsTotalCount = Instance.Events.fetch(missingCallsQuery, missingCallsPaging).totalCount; // getting total missing calls count
    16
    17    missingCallsUpdate.stream().forEach(call -> {
    18
    19        // showing notification for each new call
    20        var builder = new NotificationCompat.Builder(this, CHANNEL_ID)
    21                .setSmallIcon(R.drawable.notification_icon)
    22                .setContentTitle("Missing calls total : " + missingCallsTotalCount)
    23                .setContentText("New missing call : " + call.getRemoteUser().getDisplayName())
    24                .setPriority(NotificationCompat.PRIORITY_DEFAULT);
    25
    26        notificationManager.notify(NOTIFICATION_ID, builder.build());
    27    });
    28});
    

Enabling Logging

Enable logging to capture important events and activities that occur within the SDK.

  • By default, the logger is disabled. To enable logging, set the trafficLogging preference to true, as shown in the following code snippet:

    1Instance.preferences.trafficLogging.set(true);
    
  • To disable logging, set the trafficLogging preference to false.

  • To retrieve logs at any point, use the following code snippet:

    1String log = Instance.Log.get()
    

Integrating with Firebase

To add the push calls functionality using both libSoftphone SDK and Firebase, add Firebase Cloud Messaging (FCM) to your project, and then enable push notifications.

The FCM integration allows incoming calls to be received even when the app is asleep or in the background. The details are explain in the SIPIS section at doc.acrobits.net/sipis/index.html.

Prerequisite: Setting Up Firebase Integration

Add FCM to your project prior to implementing push calls functionality. To do so, refer to the FCM documentation at firebase.google.com/docs/cloud-messaging/android/client for the details. Once you obtain the Firebase server key of the application, contact an Acrobits representative to upload the key to Acrobits servers. The Firebase server key is required for the push calls functionality to work.

Note

For more information on setting up the VoIP push notifications in both your project and server, go to the Android Push notifications section.

Once FCM is added to your project, go to the following section to implement the push calls functionality.

Enabling Push Notifications

The push notifications function is disabled by default in libSoftphone SDK. Modify the provisioning XML file to enable and use this function with the application.

Important

Ensure that your application correctly reports its current state to the SDK. This is crucial for the push notifications to work properly. See Reporting Application States for the setup.

  1. To set the SDK registration with the push servers, add the following snippet to the prefKeys node in your provisioning XML.

    1<prop name="sipisDisabled" default="0"/>
    2<prop name="pushNotificationsEnabled" default="1"/>
    

    This snippet is passed to the Instance.init method to enable push notifications when libSoftphone SDK initializes.

  2. Once push notifications is enabled, the Instance.init method is called, and the SDK is running, perform the following steps to use the push functionality:

    • To set the incoming calls mode to push, use the following snippet:

      1Instance.preferences.incomingCallsMode.set(IncomingCallsMode.PUSH);</code>
      
    • To report the Firebase push token to the SDK, use the following snippet:

       1FirebaseInstanceId.getInstance().getInstanceId()
       2    .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>()
       3    {
       4        @Override
       5        public void onComplete(@NonNull Task<InstanceIdResult> task)
       6        {
       7            // Get new Instance ID token
       8            final String token = task.getResult().getToken();
       9            // Report token to the libSoftphone SDK
      10            AndroidUtil.handler.post(() -> Instance.Notifications.Push.setRegistrationId(token));
      11        }
      12    });
      
    • When implementing the FirebaseMessagingService, override onMessageReceived to notify libSoftphone SDK about incoming push messages with the following snippet :

       1@Override
       2public void onMessageReceived(RemoteMessage remoteMessage)
       3{
       4    AndroidUtil.rendezvous(new Runnable()
       5    {
       6        @Override
       7        public void run()
       8        {
       9            // make sure libsoftphone is initialized - call Instance.loadLibrary and Instance.init if needed
      10            Instance.Notifications.Push.handle(Xml.toXml("pushMessage", remoteMessage.getData()), remoteMessage.getPriority() == RemoteMessage.PRIORITY_HIGH);
      11        }
      12    });
      13}
      
    • To report the renewed Firebase push token to libSoftphone SDK, use the following snippet:

       1@Override
       2public void onNewToken(String s)
       3{
       4    AndroidUtil.rendezvous(new Runnable()
       5    {
       6        @Override
       7        public void run()
       8        {
       9            // make sure libsoftphone is initialized - call Instance.loadLibrary and Instance.init if needed
      10            Instance.Notifications.Push.setRegistrationId(s);
      11        }
      12    });
      13}
      
    • When the push notification of a call arrives, you are notified as in when regular calls through the registered Listeners.OnNewCall callbacks.

Testing Push Notifications

To test if push messaging are working, perform the following steps:

  1. Include the following snippet to implement the Listeners.OnPushTestArrived interface and register the callback:

    1Listeners.OnPushTestArrived onPushTestArrived = (accountId) -> Toast.makeToast(context, "Push test arrived!", Toast.LENGTH_SHORT).show();
    2mListeners.register(onPushTestArrived);
    
  2. Schedule a push test using the following snippet:

    1Instance.Notifications.Push.scheduleTest(null, 0);.
    

When the registered Listeners.OnPushTestArrived callback is triggered and displays the “Push test arrived!” message, this indicates that push notifications are working correctly.