========================================= Getting Started with Android libSoftphone ========================================= This section describes the steps to create a simple Android application using Acrobits **libSoftphone SDK**. .. contents:: :local: :depth: 1 --------------------------- 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: .. code-block:: py :linenos: maven { url "https://maven.acrobits.net/repository/maven-releases/" credentials { username password } } .. note:: * The license key - ```` - can be obtained from the portal after completing step 1 in the :ref:`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: .. code-block:: groovy :linenos: dependencies { implementation 'cz.acrobits:libsoftphone:' } .. note:: Replace ```` 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 :ref:`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: .. code-block:: groovy :linenos: @Override void onCreate(@Nullable Bundle savedState) { Instance.loadLibrary(this); super.onCreate(savedState); } * 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. .. _initialize sdk instance: 2. 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: .. code-block:: groovy :linenos: @Override void onCreate(@Nullable Bundle savedState) { Instance.init(this); //Instance.setObserver(sListeners); // to observe events super.onCreate(savedState); } * 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: .. code-block:: groovy :linenos: @Override void onCreate(@Nullable Bundle savedState) { Instance.loadLibrary(this); Xml provisioning = new Xml("provisioning"); Xml saas = new Xml("saas"); saas.replaceChild("identifier", SAAS_IDENTIFIER); provisioning.replaceChild(saas); Instance.init(this, provisioning); //Instance.setObserver(sListeners); // to observe events super.onCreate(savedState); } * 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. 3. 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: .. code-block:: :linenos: 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: .. code-block:: :linenos: Xml account = new Xml("account"); account.setAttribute(Account.Attributes.ID, "Test Account"); account.setChildValue(Account.USERNAME, userName); account.setChildValue(Account.PASSWORD, password); account.setChildValue(Account.HOST, domain); Instance.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: .. code-block:: :linenos: AccountXml accountXml = Instance.Registration.getAccount("Test Account").clone(); accountXml.setEnabled(true); Instance.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: #. Call the ``Instance.Registration.getAccount()`` method to obtain a reference to the ``AccountXml`` of the account you want to modify. #. Before making changes, call the ``AccountXml.clone()`` method to obtain a mutable reference to the account. #. Use appropriate methods or properties of the ``AccountXml`` object to make changes on the account configuration. #. 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: .. code-block:: :linenos: AccountXml accountXml = Instance.Registration.getAccount("Test Account").clone(); accountXml.mergeValue("icm", "push", MergeableNodeAttributes.gui()); Instance.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: .. code-block:: :linenos: public static final Listeners sListeners = new Listeners() { //****************************************************************** @Override public @Nullable Object getRingtone(@NonNull CallEvent call) //****************************************************************** { return RingtoneManager.getRingtone(context, Settings.System.DEFAULT_RINGTONE_URI); } }; Once you have an instance of your derived ``cz.acrobits.support.lifecycle.Listeners`` class, set it as the observer by calling ``Instance.setObserver(sListeners)``: .. code-block:: :linenos: // Instance.init() ... Instance.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. .. code-block:: :linenos: StreamParty party = new StreamParty(phoneNumber); party.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. .. code-block:: :linenos: CallEvent event = new CallEvent(party.toRemoteUser()); event.setAccount(Instance.Registration.getDefaultAccountId()); event.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``. .. code-block:: :linenos: int result = Instance.Events.post(event); if(res == Instance.Events.PostResult.SUCCESS) { Toast.makeToast(context, "Call placed succesfully", Toast.LENGTH_SHORT).show(); } else { Toast.makeToast(context, String.format("Call failed: %d", res), Toast.LENGTH_SHORT).show(); } -------------------- 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 trafficLogging = new Key<>("sipTrafficLogging")``, or * Add the code snippet ``final Preferences.Key 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: .. code-block:: :linenos: if (Instance.preferences.trafficLogging.get()) { Toast.makeText(context, "Logging is enabled", Toast.LENGTH_SHORT).show(); } ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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: .. code-block:: :linenos: Instance.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: .. code-block:: :linenos: class SettingsListener implements OnSettingsChanged { @Override public void onSettingsChanged() { if (Instance.preferences.trafficLogging.hasChanged()) { Toast.makeText(context, String.format("Logging is now set to %b", Instance.preferences.trafficLogging.get()), Toast.LENGTH_SHORT).show(); } } } // ... sListeners.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. #. To enable the Call Event History feature, add the following code in the app's provisioning XML file: .. code-block:: groovy :linenos: your license key You can refer to the `Initializing the SDK`_ section to review the steps for supplying the provisioning file during SDK initialization. #. 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: .. code-block:: groovy :linenos: Listeners listeners = new Listeners() { @Nullable @Override public Object getRingtone(@NonNull CallEvent callEvent) { return null; } }; // Instance.setObserver(sListeners) ... listeners.register((Listeners.OnEventsChanged) (changedEvents, changedStreams) -> { // From here, you can trigger a refresh of the call history list in your app // get changed events - only set if changedEvents.many is false long[] changedIds = changedEvents.eventIds; // id's of changed events // check if many events changed, if true, changedIds is null boolean manyChanged = changedEvents.many; // check if any call event changed boolean callEventsChanged = changedStreams.streamKeys != null && Arrays.stream(changedStreams.streamKeys) .collect(Collectors.toList()) .contains(StreamQuery.legacyCallHistoryStreamKey()); }); // Register onMissingCalls listener listeners.register((Listeners.OnMissedCalls) callEvents -> { // triggers in case of any new missing call entry(s) appears in history) List missingCallsUpdate = Arrays.stream(callEvents) .collect(Collectors.toList()); // new missing calls detected (in most cases is one call) // this can be used for display missing call notification for example }); // unregister listener later when not in use anymore // listeners.unregister(...); Instance.setObserver(listeners); #. 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: .. code-block:: groovy :linenos: CallEventQuery query = new CallEventQuery(); // empty query for calls query.streamKey = StreamQuery.legacyCallHistoryStreamKey(); // set query only calls // apply a filter if needed, here we are filtering only missed calls query.resultMask = CallEvent.Result.MISSED; // set query only missing calls // optional paging for query results EventPaging paging = new EventPaging(); /*paging.after = ...; paging.before = ...; paging.olderThan = ...; paging.offset = ...; paging.limit = ...; etc...*/ // fetch the call events EventFetchResult result = Instance.Events.fetch(query, paging); // get missed calls count int totalMissingCalls = result.totalCount; // get missed calls list List missingCalls = Arrays.stream(result.events) .map(CallEvent.class::cast) .collect(Collectors.toList()); #. 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: .. code-block:: groovy :linenos: listeners.register((Listeners.OnMissedCalls) callEvents -> { // triggers in case of any new missing call entry(s) appears in history List missingCallsUpdate = Arrays.stream(callEvents) .collect(Collectors.toList()); // new missing calls detected (in most cases is one call) // this can be used for display missing call notification for example CallEventQuery missingCallsQuery = new CallEventQuery(); // Query for total count of missing calls query.streamKey = StreamQuery.legacyCallHistoryStreamKey(); query.resultMask = CallEvent.Result.MISSED; EventPaging missingCallsPaging = new EventPaging(); missingCallsPaging.limit = 1; // we need only count so can limit page to one item int missingCallsTotalCount = Instance.Events.fetch(missingCallsQuery, missingCallsPaging).totalCount; // getting total missing calls count missingCallsUpdate.stream().forEach(call -> { // showing notification for each new call var builder = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.notification_icon) .setContentTitle("Missing calls total : " + missingCallsTotalCount) .setContentText("New missing call : " + call.getRemoteUser().getDisplayName()) .setPriority(NotificationCompat.PRIORITY_DEFAULT); notificationManager.notify(NOTIFICATION_ID, builder.build()); }); }); ---------------- 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: .. code-block:: :linenos: Instance.preferences.trafficLogging.set(true); * To disable logging, set the ``trafficLogging`` preference to ``false``. * To retrieve logs at any point, use the following code snippet: .. code-block:: :linenos: String 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 :ref:`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. .. code-block:: :linenos: 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: .. code-block:: :linenos: Instance.preferences.incomingCallsMode.set(IncomingCallsMode.PUSH); * To report the Firebase push token to the SDK, use the following snippet: .. code-block:: :linenos: FirebaseInstanceId.getInstance().getInstanceId() .addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { // Get new Instance ID token final String token = task.getResult().getToken(); // Report token to the libSoftphone SDK AndroidUtil.handler.post(() -> Instance.Notifications.Push.setRegistrationId(token)); } }); * When implementing the ``FirebaseMessagingService``, override ``onMessageReceived`` to notify libSoftphone SDK about incoming push messages with the following snippet : .. code-block:: :linenos: @Override public void onMessageReceived(RemoteMessage remoteMessage) { AndroidUtil.rendezvous(new Runnable() { @Override public void run() { // make sure libsoftphone is initialized - call Instance.loadLibrary and Instance.init if needed Instance.Notifications.Push.handle(Xml.toXml("pushMessage", remoteMessage.getData()), remoteMessage.getPriority() == RemoteMessage.PRIORITY_HIGH); } }); } * To report the renewed Firebase push token to libSoftphone SDK, use the following snippet: .. code-block:: :linenos: @Override public void onNewToken(String s) { AndroidUtil.rendezvous(new Runnable() { @Override public void run() { // make sure libsoftphone is initialized - call Instance.loadLibrary and Instance.init if needed Instance.Notifications.Push.setRegistrationId(s); } }); } * 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: .. code-block:: :linenos: Listeners.OnPushTestArrived onPushTestArrived = (accountId) -> Toast.makeToast(context, "Push test arrived!", Toast.LENGTH_SHORT).show(); mListeners.register(onPushTestArrived); 2. Schedule a push test using the following snippet: .. code-block:: :linenos: Instance.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.