Android softphone: Predictive dialer

The following guide introduces how to develop a softphone, which handles a ring group (group of agents), and a list of clients, who are need to be called. The main function of this example application is to call the clients from the list one by one, than render an agent to them. Let's get started!

Related: VoIP SIP predictive dialer. If also you wish to build VoIP softphone on Windows, you might also be interested in a similar document, the VoIP SIP Predictive Dialer in Ozeki VoIP SDK for Windows

To fully understand this guide, you might have to visit the Android softphone: auto dialer article first, since this guide is about to continue to develop that.

Quick steps

  1. Download and install the Ozeki Andorid SDK
  2. Create a new Xamarin Android application in Viusal Studio
  3. Add the OzekiSDK.dll as a reference to our project
  4. Create the layout of the application
  5. Create a .csv file to store the numbers and the messages
  6. Create a Softphone class
  7. Create the CallInfo class that will be used to store the calls
  8. Create a ConfigStore class
  9. Create the CallHandler class that will be used for handling the call events
  10. Create the AutoDialer class which will be responsible for making the automated calls from a list
  11. Create the MainActivity.cs activity which will ask for the login details
  12. Create the DialingActivity.cs file which will be responsible for receiving calls.
  13. Run the example application and receive multiple calls in the same time.

The 09_PredictiveDialer.zip file

You can download the 09_PredictiveDialer example application bellow, and start receiving calls with it right now, or you can build it yourself step by step to understand the base concept of the Ozeki VoIP SIP SDK.

Download: 09_PredictiveDialer.zip (2.18Mb)

Create a new Xamarin Android application in Visual Studio

To build our softphone application, we will create a new Xamarin Andorid project. In the following video I'll show you how to create a new Xamarin application in Visual Studio.

Video 1 - How to create a new Xamarin Andorid project in Visual Studio Community

How to add the OzekiSDK.dll to our project as a reference

In order to use the contents of the Ozeki.VoIP namespace, we have to include the OzekiSDK.dll in our project. In the following video I'll show you how to add the OzekiSDK.dll reference to your project. You can find the OzekiSDK.dll file in the following place: C:\Program Files\Ozeki\Ozeki SDK for Android\SDK\MonoAndroid.

how to add a dll reference to a visual studio project
Figure 1 - Right click on the "References" folder, than choose the "Add Reference..." option

how to add a dll reference to a visual studio project
Figure 2 - Click on the Browse... button, and search for the OzekiSDK.dll file on your disk

how to add a dll reference to a visual studio project
Figure 3 - Select the .dll file, and click on the Add button

how to add a dll reference to a visual studio project
Figure 4 - After you have added the reference, you can import the components of the OzekiSDK.dll file

Video 2 - How to add the OzekiSDK dll reference to you project

The layout of the application

For the MainActivity activity we are going to use the layout of the Android softphone: SIP registration example. This activity will be responsible for the SIP registration. For the DialingActivity activity we have created a layout, which will display the phone numbers stored in the ExampleCSV.csv file and all the agents we have added to the calls.

You can simply drag and drop these .xml files into you layout folder, or you can create your own layout.

Download: layout.zip (1.89Kb)

In order to create the dorpdown list, you also have to copy and pase this array.xml file into the values folder of your application.

Download: array.zip (237B)

Video 3 - How to add the .xml files below to your project

		<?xml version="1.0" encoding="utf-8"?>
		<androidx.coordinatorlayout.widget.CoordinatorLayout
			xmlns:android="http://schemas.android.com/apk/res/android"
		    xmlns:app="http://schemas.android.com/apk/res-auto"
		    xmlns:tools="http://schemas.android.com/tools"
		    android:layout_width="match_parent"
		    android:layout_height="match_parent">
		
		    <GridLayout
		        android:layout_width="match_parent"
		        android:layout_height="match_parent"
		        android:rowCount="9"
		    >
		
		        <TextView
		            android:layout_width="300dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_height="wrap_content"
		            android:layout_row="0"
		            android:text="SIP registration"
		            android:textSize="12pt"
		            android:textAlignment="center"
		            android:layout_marginTop="10dp"
		        />
		
		        <EditText
		            android:id="@+id/inputDisplayName"
		            android:layout_width="300dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_height="wrap_content"
		            android:layout_column="0"
		            android:layout_row="1"
		            android:hint="Display name"
		            android:inputType="text"
		        />
		
		        <EditText
		            android:id="@+id/inputUserName"
		            android:layout_width="300dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_height="wrap_content"
		            android:layout_column="0"
		            android:layout_row="2"
		            android:hint="Username"
		            android:inputType="text"
		        />
		
		        <EditText
		            android:id="@+id/inputAuthenticationID"
		            android:layout_width="300dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_height="wrap_content"
		            android:layout_column="0"
		            android:layout_row="3"
		            android:hint="Authentication ID"
		            android:inputType="text"
		        />
		
		        <EditText
		            android:id="@+id/inputRegisterPassword"
		            android:layout_width="300dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_height="wrap_content"
		            android:layout_column="0"
		            android:layout_row="4"
		            android:inputType="textPassword"
		            android:hint="Password"
		        />
		
		        <EditText
		            android:id="@+id/inputDomainHost"
		            android:layout_width="300dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_height="wrap_content"
		            android:layout_column="0"
		            android:layout_row="5"
		            android:hint="Host e.g.: 127.0.0.1"
		            android:inputType="text"
		        />
		
		        <EditText
		            android:id="@+id/inputDomainPort"
		            android:layout_width="300dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_height="wrap_content"
		            android:layout_column="0"
		            android:layout_row="6"
		            android:hint="Port e.g.: 5060"
		            android:inputType="text"
		        />
		
		        <Button
		            android:id="@+id/btnLogin"
		            android:layout_width="300dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_height="wrap_content"
		            android:layout_column="0"
		            android:backgroundTint="@android:color/holo_red_light"
		            android:textColor="@android:color/white"
		            android:layout_row="7"
		            android:text="Login"
		        />
		
		        <GridLayout
		            android:layout_width="300dp"
		            android:layout_height="wrap_content"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_row="8"
		            android:layout_column="0"
		            android:rowCount="2"
		            android:columnCount="1"
		        >
		
		            <TextView
		                android:layout_width="match_parent"
		                android:layout_height="wrap_content"
		                android:textSize="10pt"
		                android:text="Log:"
		                android:layout_row="0"/>
		
		            <TextView
		                android:id="@+id/log"
		                android:layout_width="match_parent"
		                android:layout_height="120dp"/>
		
		        </GridLayout>
		
		    </GridLayout>
		
		</androidx.coordinatorlayout.widget.CoordinatorLayout>
	

Code 1 - activity_main.xml

		<?xml version="1.0" encoding="utf-8"?>
		<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
		    xmlns:app="http://schemas.android.com/apk/res-auto"
		    xmlns:tools="http://schemas.android.com/tools"
		    android:layout_width="match_parent"
		    android:layout_height="match_parent"
		    app:layout_behavior="@string/appbar_scrolling_view_behavior">
		
		    <GridLayout
		        android:layout_width="match_parent"
		        android:layout_height="wrap_content"
		        android:rowCount="11"
		        android:columnCount="1"
		    >
		
		        <TextView
		            android:id="@+id/btnLogOut"
		            android:layout_width="match_parent"
		            android:layout_marginTop="10dp"
		            android:layout_marginLeft="10dp"
		            android:layout_height="wrap_content"
		            android:text="Logout"
		            android:layout_row="0"
		            android:layout_column="0"
		        />
		       
		        <TextView
		            android:layout_width="300dp"
		            android:layout_height="50dp"
		            android:layout_marginTop="25dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:textAlignment="center"
		            android:textSize="12pt"
		            android:layout_row="1"
		            android:layout_column="0"
		            android:text="PredictiveDialer"/>
		
		        <TextView
		            android:id="@+id/status"
		            android:layout_width="250dp"
		            android:layout_height="30dp"
		            android:textAlignment="center"
		            android:layout_row="2"
		            android:layout_column="0"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:text="Not registered"
		        />
		
		        <TextView
		            android:layout_width="350dp"
		            android:layout_height="30dp"
		            android:layout_row="3"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_column="0"
		            android:text="Numbers to call:"/>
		
		        <TextView
		            android:id="@+id/listOfNumbers"
		            android:layout_width="350dp"
		            android:layout_height="150dp"
		            android:layout_row="4"
		            android:layout_column="0"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:text=""
		        />
		
		        <TextView
		            android:layout_width="350dp"
		            android:layout_height="30dp"
		            android:layout_row="5"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:layout_column="0"
		            android:text="Agents:"/>
		
		        <TextView
		            android:id="@+id/listOfAgents"
		            android:layout_width="350dp"
		            android:layout_height="100dp"
		            android:layout_row="6"
		            android:layout_column="0"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:text=""
		        />
		
		        <GridLayout
		            android:layout_width="300dp"
		            android:layout_height="wrap_content"
		            android:layout_row="7"
		            android:layout_column="0"
		            android:rowCount="1"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:columnCount="2">
		
		            <TextView
		                android:layout_width="175dp"
		                android:layout_height="match_parent"
		                android:gravity="center_vertical|left"
		                android:text="Add agent:"
		                android:layout_column="0"
		                android:layout_row="0"
		            />
		            <EditText
		                android:id="@+id/inputAgentNumber"
		                android:layout_width="125dp"
		                android:layout_height="wrap_content"
		                android:layout_column="1"
		                android:layout_row="0"
		                android:hint="1001"
		            />
		        </GridLayout>
		            
		        <Button 
		            android:id="@+id/btnAddAgent"
		            android:layout_height="50dp"
		            android:layout_width="250dp"
		            android:text="ADD AGENT"
		            android:layout_column="0"
		            android:layout_row="8"
		            android:layout_margin="2dp"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:backgroundTint="@android:color/holo_red_light"
		            android:textColor="@android:color/white"
		        />
		
		        <GridLayout
		            android:layout_width="300dp"
		            android:layout_height="wrap_content"
		            android:layout_row="9"
		            android:layout_column="0"
		            android:rowCount="1"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:columnCount="2">
		
		            <TextView
		                android:layout_width="175dp"
		                android:layout_height="40dp"
		                android:gravity="center_vertical|left"
		                android:text="Max concurrent calls:"
		                android:layout_column="0"
		                android:layout_row="0"
		            />
		
		            <Spinner
		                android:id="@+id/maxConcurrentCallSpinner"
		                android:layout_width="125dp"
		                android:layout_height="40dp"
		                android:layout_column="1"
		                android:layout_row="0"
		                android:entries="@array/maxConcurrentCallsSpinner"
		            />
		        </GridLayout>
		
		        <Button
		            android:id="@+id/btnInteractions"
		            android:layout_width="250dp"
		            android:layout_height="wrap_content"
		            android:layout_row="10"
		            android:layout_column="0"
		            android:layout_margin="2dp"
		            android:backgroundTint="@android:color/holo_red_light"
		            android:textColor="@android:color/white"
		            android:layout_gravity="center_vertical|center_horizontal"
		            android:text="Dial"
		            android:state_enabled="false"
		        />
		
		    </GridLayout>
		
		</RelativeLayout>
	

Code 2 - activity_dialing.xml

		<resources>
		  <string-array name="maxConcurrentCallsSpinner">
		    <item>1</item>
		    <item>2</item>
		    <item>3</item>
		    <item>4</item>
		    <item>5</item>
		    <item>6</item>
		    <item>7</item>
		    <item>8</item>
		    <item>9</item>
		    <item>10</item>
		  </string-array>
		</resources>
	

Code 3 - array.xml

Create the Softphone.cs file

With the Softphone class, you can initialize a sofphone instance which will be responsible for making calls and performing a SIP account registration. In the following video you can see how to create the Softphone class.

Video 5 - Creating the Softphone class

Softphone.cs

		using System;
		using Ozeki.VoIP;
		
		namespace _09_PredictiveDialer
		{
		    internal class Softphone
		    {
		        ISoftPhone _softphone;
		        IPhoneLine _phoneLine;
		
		        public Softphone()
		        {
		            _softphone = SoftPhoneFactory.CreateSoftPhone(5000, 10000);
		        }
		
		        public void Register(bool registrationRequired, string displayName,
		        	string userName, string authenticationId, string registerPassword,
		        	string domainHost, int domainPort)
		        {
		            try
		            {
		                var account = new SIPAccount(registrationRequired, displayName,
		                	userName, authenticationId, registerPassword,
		                	domainHost, domainPort);
		
		                _phoneLine = _softphone.CreatePhoneLine(account);
		
		                _phoneLine.RegistrationStateChanged +=_phoneLine_RegistrationStateChanged;
		
		                _softphone.RegisterPhoneLine(_phoneLine);
		            }
		            catch (Exception ex)
		            {
		                Console.WriteLine("Error during SIP registration: {0}", ex.ToString());
		            }
		        }
		
		        public void Unregister()
		        {
		            _softphone.UnregisterPhoneLine(_phoneLine);
		        }
		
		        void _phoneLine_RegistrationStateChanged(object sender, RegistrationStateChangedArgs e)
		        {
		            var handler = PhoneLineStateChanged;
		            if (handler != null)
		                handler(this, e);
		        }
		
		        public IPhoneCall CreateCall(string member)
		        {
		            return _softphone.CreateCallObject(_phoneLine, member);
		        }
		
		        public event EventHandler<RegistrationStateChangedArgs> PhoneLineStateChanged;
		    }
		}
	

Code 4 - Softphone.cs

Create the CallInfo.cs file

Each CallInfo object represents a line within the csv file, which is being used as a complex value, as it stores a phone number and a message which being used as a note about the client in this example. CallHandler objects will be created for all of the CallInfo objects, to manage the calls separately. In the following video you can see how to create the CallInfo class.

Video 4 - Creating the CallInfo class

CallInfo.cs

		namespace _09_PredictiveDialer
		{
		    class CallInfo
		    {
		        public string PhoneNumber { get; private set; }
		        public string Message { get; private set; }
		
		        public CallInfo(string phoneNumber, string message)
		        {
		            PhoneNumber = phoneNumber;
		            Message = message;
		        }
		    }
		}
	

Code 5 - CallInfo.cs

Create the ConfigStore.cs file

We will use the ConfigStore class to store the lists and provide access to it's method through one instance, the application is using this class. It stores three lists, and provides the neccessary methods to reach, add or remove elements from it:

  • List of agents: stores all of the agents of the ring group, and the class provides a method to add new agents to the list. The program class uses this method, when asks the user about the agents.
  • List of agents: stores the available agents, who are not in call at the moment. Provides three methods: to remove or add an agent, and to get the available ones. The CallHandler class is using these methods, since it works with the available agents' list.
  • List of clients to be called: stores the client's phone numbers and notes (messages) as CallInfo objects, and also provides two methods: one to add new clients, and one to get the list of them. The Program class is using the adder method of this list when asks the user about a csv file, and the CallHandler class is using the list to call the clients one-by-one.
In the following video you can see how to create the ConfigStore class.

Video 5 - Creating the ConfigStore class

ConfigStore.cs

		using System;
		using System.Collections.Generic;
		
		namespace _09_PredictiveDialer
		{
		    class ConfigStore
		    {
		        static ConfigStore _instance = new ConfigStore();
		
		        object _sync;
		        List<String> _agentList;
		        List<CallInfo> _callList;
		        List<String> _freeAgents;
		
		        ConfigStore()
		        {
		            _sync = new object();
		            _callList = new List<CallInfo>();
		            _freeAgents = new List<string>();
		            _agentList = new List<string>();
		        }
		
		        public static ConfigStore Instance
		        {
		            get { return _instance; }
		        }
		
		        public void AddToCallList(CallInfo callInfo)
		        {
		            lock (_sync)
		                _callList.Add(callInfo);
		        }
		
		        public List<CallInfo> GetCallList()
		        {
		            lock (_sync)
		                return new List<CallInfo>(_callList);
		        }
		
		        public void AddAgent(string agent)
		        {
		            lock (_sync)
		                _agentList.Add(agent);
		        }
		
		        public void RemoveFreeAgent(string agent)
		        {
		            lock (_sync)
		                _freeAgents.Remove(agent);
		        }
		
		        public void AddFreeAgent(string agent)
		        {
		            lock (_sync)
		                _freeAgents.Add(agent);
		        }
		
		        public List<string> GetFreeAgents()
		        {
		            lock (_sync)
		                return new List<string>(_freeAgents);
		        }
		    }
		}
	

Code 6 - ConfigStore.cs

Create the CallHandler.cs file

The softphone can handle multiple calls simultaneously, and each of those is being handled by a CallHandler instance, set by a CallInfo object. Since a CallInfo object stores a phone number, the call will be created to that number, and if the call is being accepted by the client, a ring group group will be called with the RingAll strategy. To read more about ring group implementation and ring strategies, please read the Ring Group guide.

When a group agents answeres the call, the audiodata will be going through this softphone, instead of transfering the calls to each other. In the following video you can see how to create the CallHandler class.

Video 6 - Creating the CallHandler class

CallHandler.cs

		using System;
		using System.Collections.Generic;
		using System.Threading;
		using Ozeki.Media;
		using Ozeki.VoIP;
		
		namespace _09_PredictiveDialer
		{
		    internal class CallHandler
		    {
		        private Softphone _softphone;
		        private CallInfo _callInfo;
		        private IPhoneCall _call;
		        private IPhoneCall _agentCall;
		
		        private MediaConnector _connectorFromAgent;
		        private MediaConnector _connectorFromClient;
		        private PhoneCallAudioSender _mediaSenderToAgent;
		        private PhoneCallAudioSender _mediaSenderToClient;
		        private PhoneCallAudioReceiver _mediaReceiverFromAgent;
		        private PhoneCallAudioReceiver _mediaReceiverFromClient;
		
		        private AutoResetEvent _autoResetEvent;
		
		        private List<IPhoneCall> _freeAgentChecks;
		
		        private bool _needToHangUp;
		
		        private int _freeAgents;
		
		        private object _sync;
		        private ConfigStore _configStore;
		
		        public CallHandler(Softphone softphone, CallInfo callInfo)
		        {
		            _softphone = softphone;
		            _callInfo = callInfo;
		            _configStore = ConfigStore.Instance;
		
		            _connectorFromAgent = new MediaConnector();
		            _connectorFromClient = new MediaConnector();
		            _mediaSenderToAgent = new PhoneCallAudioSender();
		            _mediaSenderToClient = new PhoneCallAudioSender();
		            _mediaReceiverFromAgent = new PhoneCallAudioReceiver();
		            _mediaReceiverFromClient = new PhoneCallAudioReceiver();
		
		            _freeAgentChecks = new List<IPhoneCall>();
		
		            _autoResetEvent = new AutoResetEvent(false);
		
		            _needToHangUp = true;
		
		            _sync = new object();
		        }
		
		        public void Start()
		        {
		            lock (_sync)
		            {
		                _call = _softphone.CreateCall(_callInfo.PhoneNumber);
		                _call.CallStateChanged += ClientCallStateChanged;
		                _mediaReceiverFromClient.AttachToCall(_call);
		                _mediaSenderToAgent.AttachToCall(_call);
		                _connectorFromClient.Connect(_mediaReceiverFromClient, _mediaSenderToAgent);
		                _call.Start();
		            }
		        }
		
		        public event EventHandler Completed;
		        public event EventHandler ReadyToCall;
		
		        private void CallRingGroup()
		        {
		            if (_configStore.GetFreeAgents().Count > 0)
		            {
		                CheckAgents();
		            }
		            else
		            {
		                _autoResetEvent.WaitOne();
		                CheckAgents();
		            }
		        }
		
		        private void CheckAgents()
		        {
		            lock (_sync)
		            {
		                _freeAgents = 0;
		                foreach (var freeAgent in _configStore.GetFreeAgents())
		                {
		                    var callAgent = _softphone.CreateCall(freeAgent);
		                    callAgent.CallStateChanged += AgentCallStateChanged;
		                    _freeAgentChecks.Add(callAgent);
		                    _freeAgents++;
		                }
		
		                foreach (var freeAgentCheck in _freeAgentChecks)
		                {
		                    freeAgentCheck.Start();
		                }
		            }
		        }
		
		        private void SetupAgentDevices(IPhoneCall call)
		        {
		            lock (_sync)
		            {
		                _agentCall = call;
		                _mediaReceiverFromAgent.AttachToCall(call);
		                _mediaSenderToClient.AttachToCall(call);
		                _connectorFromAgent.Connect(_mediaReceiverFromAgent, _mediaSenderToClient);
		            }
		        }
		
		        private void DestructClientDevices()
		        {
		            lock (_sync)
		            {
		                _call.CallStateChanged -= ClientCallStateChanged;
		                _mediaReceiverFromClient.Detach();
		                _mediaSenderToAgent.Detach();
		                _connectorFromClient.Dispose();
		                _call = null;
		            }
		        }
		
		        private void DestructAgentDevices(IPhoneCall call)
		        {
		            lock (_sync)
		            {
		                call.CallStateChanged -= AgentCallStateChanged;
		                call.HangUp();
		
		                if (_agentCall == call)
		                {
		                    _mediaReceiverFromAgent.Detach();
		                    _mediaSenderToClient.Detach();
		                    _connectorFromAgent.Dispose();
		                }
		            }
		        }
		
		        private void ClientCallStateChanged(object sender, CallStateChangedArgs e)
		        {
		            if (e.State == CallState.Answered)
		            {
		                lock (_sync)
		                {
		                    CallRingGroup();
		                }
		            }
		            else if (e.State == CallState.RemoteHeld && _agentCall != null)
		            {
		                _agentCall.Hold();
		            }
		            else if (e.State.IsInCall() && _agentCall != null)
		            {
		                if (_agentCall.CallState == CallState.LocalHeld ||
		                _agentCall.CallState == CallState.InactiveHeld)
		                    _agentCall.Unhold();
		            }
		            else if (e.State.IsCallEnded())
		            {
		                lock (_sync)
		                {
		                    DestructClientDevices();
		
		                    if (_needToHangUp && _agentCall != null)
		                    {
		                        _needToHangUp = false;
		                        _agentCall.HangUp();
		                    }
		
		                    var handler = Completed;
		                    if (handler != null)
		                        handler(this, EventArgs.Empty);
		
		                    HangUpAgents();
		                }
		            }
		        }
		
		        private void AgentCallStateChanged(object sender, CallStateChangedArgs e)
		        {
		            var currentCall = (IPhoneCall) sender;
		
		            if (e.State == CallState.Answered)
		            {
		                lock (_sync)
		                {
		                    SetupAgentDevices(currentCall);
		                    _agentCall = currentCall;
		
		                    _configStore.RemoveFreeAgent(currentCall.DialInfo.Dialed);
		                    _freeAgentChecks.Remove(currentCall);
		
		                    HangUpAgents();
		
		                    var handler = ReadyToCall;
		                    if (handler != null)
		                        handler(this, EventArgs.Empty);
		                }
		            }
		            else if (e.State == CallState.RemoteHeld)
		            {
		                _call.Hold();
		            }
		            else if (e.State.IsInCall())
		            {
		                if (_call.CallState == CallState.LocalHeld ||
		                _call.CallState == CallState.InactiveHeld)
		                    _call.Unhold();
		            }
		            else if (e.State.IsCallEnded())
		            {
		                lock (_sync)
		                {
		                    _freeAgents--;
		
		                    if (!_configStore.GetFreeAgents().Contains(currentCall.DialInfo.Dialed))
		                    {
		                        if (_needToHangUp)
		                        {
		                            _needToHangUp = false;
		                            _call.HangUp();
		                        }
		                        _configStore.AddFreeAgent(currentCall.DialInfo.Dialed);
		                        _autoResetEvent.Set();
		                    }
		                    DestructAgentDevices(currentCall);
		
		                    if (_freeAgents == 0)
		                    {
		                        HangUpClient();
		                        var handler = ReadyToCall;
		                        if (handler != null)
		                            handler(this, EventArgs.Empty);
		                    }
		                }
		            }
		        }
		
		        private void HangUpClient()
		        {
		            if (_needToHangUp)
		            {
		                _needToHangUp = false;
		                _call.HangUp();
		            }
		        }
		
		        private void HangUpAgents()
		        {
		            lock (_sync)
		            {
		                foreach (var checkedAgent in _freeAgentChecks)
		                {
		                    checkedAgent.HangUp();
		                    DestructAgentDevices(checkedAgent);
		                }
		                _freeAgentChecks.Clear();
		            }
		        }
		    }
		}
	

Code 7 - CallHandler.cs

Create the Autodialer.cs file

The Autodialer class creates the CallHandler instances from CallInfo objects and starts them by calling their Start() method. The class also listens to the CallHandler class's events, which indicate when a client call has been accepted or ended.

To learn more about the Autodialer class, please visit the Autodialer article. In the following video you can see how to create the Autodialer class.

Video 7 - Creating the Autodialer class

Autodialer.cs

		using System;
		using System.Collections.Generic;
		using System.Threading;
		
		namespace _09_PredictiveDialer
		{
		    class Autodialer
		    {
		        Softphone _softphone;
		        List<CallInfo> _callList;
		        int _maxConcurrentCall;
		        int _currentConcurrentCall;
		        List<CallHandler> _callHandlers;
		        AutoResetEvent _autoResetEvent;
		        AutoResetEvent _nextCall;
		        object _sync;
		
		        bool _firstCall;
		
		        public Autodialer(Softphone softphone, List<CallInfo> callList, int maxConcurrentCall)
		        {
		            _sync = new object();
		            _softphone = softphone;
		            _callList = callList;
		            _maxConcurrentCall = maxConcurrentCall;
		            _callHandlers = new List<CallHandler>();
		            _autoResetEvent = new AutoResetEvent(false);
		            _nextCall = new AutoResetEvent(false);
		
		            _firstCall = true;
		        }
		
		        public void Start()
		        {
		            StartCall();
		        }
		
		        void StartCall()
		        {
		            ThreadPool.QueueUserWorkItem(o =>
		            {
		                if (_callList.Count > 0)
		                {
		                    foreach (var callInfo in _callList)
		                    {
		                        if (_currentConcurrentCall < _maxConcurrentCall)
		                        {
		                            if (_firstCall)
		                            {
		                                _firstCall = false;
		                                StartCallHandler(callInfo);
		                            }
		                            else
		                            {
		                                _nextCall.WaitOne();
		                                StartCallHandler(callInfo);
		                            }
		                        }
		                        else
		                        {
		                            _autoResetEvent.WaitOne();
		                            StartCallHandler(callInfo);
		                        }
		                    }
		                }
		            });
		        }
		
		        void StartCallHandler(CallInfo callInfo)
		        {
		            lock (_sync)
		            {
		                ++_currentConcurrentCall;
		                var callHandler = new CallHandler(_softphone, callInfo);
		                callHandler.Completed += callHandler_Completed;
		                callHandler.ReadyToCall += callHandler_ReadyToCall;
		                _callHandlers.Add(callHandler);
		
		                callHandler.Start();
		            }
		        }
		
		        void callHandler_ReadyToCall(object sender, EventArgs e)
		        {
		            _nextCall.Set();
		        }
		
		        void callHandler_Completed(object sender, EventArgs e)
		        {
		            lock (_sync)
		            {
		                _callHandlers.Remove((CallHandler)sender);
		                --_currentConcurrentCall;
		                _autoResetEvent.Set();
		            }
		        }
		
		    }
		}
	

Code 8 - Autodialer.cs

Create the MainActivity.cs file

This activity will be responsible for collecting the login details of the phone extension. After if have collected the credentials, it will store the values in the Xamarin.Essentials.Preferences, what you can use throughout the whole application.

Video 8 - Creating the MainActivity.cs file

MainActivty.cs

		using System;
		using Android.App;
		using Android.OS;
		using Android.Runtime;
		using Android.Views;
		using AndroidX.AppCompat.App;
		using Android.Widget;
		
		namespace _09_PredictiveDialer
		{
		    [Activity(Label = "@string/app_name", Theme = "@style/AppTheme.NoActionBar", MainLauncher = true)]
		    public class MainActivity : AppCompatActivity
		    {
		        Button _btnLogin;
		        TextView _log;
		        protected override void OnCreate(Bundle savedInstanceState)
		        {
		            base.OnCreate(savedInstanceState);
		            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
		            SetContentView(Resource.Layout.activity_main);
		
		            TestIfUserLoggedIn();
		
		            _btnLogin = FindViewById<Button>(Resource.Id.btnLogin);
		            _btnLogin.Click += OnClick__btnLogin;
		
		            _log = FindViewById<TextView>(Resource.Id.log);
		        }
		
		        private void OnClick__btnLogin(object sender, EventArgs e)
		        {
		            string displayName = FindViewById<EditText>(Resource.Id.inputDisplayName).Text;
		            string userName = FindViewById<EditText>(Resource.Id.inputUserName).Text;
		            string authenticationId = FindViewById<EditText>(Resource.Id.inputAuthenticationID).Text;
		            string registerPassword = FindViewById<EditText>(Resource.Id.inputRegisterPassword).Text;
		            string domainHost = FindViewById<EditText>(Resource.Id.inputDomainHost).Text;
		            int domainPort = -1;
		
		            try
		            {
		                domainPort = Int32.Parse(FindViewById<EditText>(Resource.Id.inputDomainPort).Text);
		            }
		            catch (Exception exception)
		            {
		                _log.Text += "Please provide a valid PORT number!\n";
		            }
		
		            if (!string.IsNullOrEmpty(displayName) && !string.IsNullOrEmpty(userName)
		                && !string.IsNullOrEmpty(authenticationId) && !string.IsNullOrEmpty(registerPassword)
		                && !string.IsNullOrEmpty(domainHost) && domainPort != -1)
		            {
		                /*
		                    In this part of the code we use the Xamarin.Essentials.Preferences
		                    to store our session details.
		                */
		
		                Xamarin.Essentials.Preferences.Set("display_name", displayName);
		                Xamarin.Essentials.Preferences.Set("user_name", userName);
		                Xamarin.Essentials.Preferences.Set("authentication_id", authenticationId);
		                Xamarin.Essentials.Preferences.Set("register_password", registerPassword);
		                Xamarin.Essentials.Preferences.Set("domain_host", domainHost);
		                Xamarin.Essentials.Preferences.Set("domain_port", domainPort);
		
		                StartActivity(typeof(DialingActivity));
		            }
		            else
		            {
		                _log.Text += "Please fill all the required fields!\n";
		            }
		        }
		
		        /*
		            This method tests if the user logged in, by testing if the
		            Xamarin.Essentials.Preferences contains the "display_name" key.
		            If it doesn't, it means that we do not have a stored session because
		            when we press the logout button in the MessaingActivity, it clears
		            the Preferences.
		        */
		
		        private void TestIfUserLoggedIn()
		        {
		            if (Xamarin.Essentials.Preferences.ContainsKey("display_name"))
		            {
		                StartActivity(typeof(DialingActivity));
		            }
		        }
		
		        public override bool OnCreateOptionsMenu(IMenu menu)
		        {
		            MenuInflater.Inflate(Resource.Menu.menu_main, menu);
		            return true;
		        }
		
		        public override bool OnOptionsItemSelected(IMenuItem item)
		        {
		            int id = item.ItemId;
		            if (id == Resource.Id.action_settings)
		            {
		                return true;
		            }
		
		            return base.OnOptionsItemSelected(item);
		        }
		
		        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
		        {
		            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
		
		            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
		        }
		    }
		}
	

Code 9 - MainActivty.cs

Create the DialingActivity.cs file

This activity will be responsible for making the automated calls. Above the button you can see all the phone numbers and the messages that you put in the .csv file. In the following video I'll show you how to create the DialingActivity activity.

Video 9 - Creating the DialingActivity activity

DialingActivty.cs

		using Android.App;
		using Android.OS;
		using Android.Widget;
		using System;
		using System.Collections.Generic;
		using System.Text;
		using Ozeki.VoIP;
		using Android.Content.Res;
		using System.IO;
		using Android.Text.Method;
		
		namespace _09_PredictiveDialer
		{
		    [Activity(Label = "DialingActivity")]
		    public class DialingActivity : Activity
		    {
		        Softphone _mySoftphone;
		        ConfigStore _configStore;
		        Autodialer _autoDialer;
		        CallInfo _callInfo;
		        List<CallInfo> _callList;
		
		        bool _registered;
		        string _filename;
		        int _maxConcurrentCall;
		
		        TextView _status;
		        TextView _btnLogout;
		        TextView _listOfNumbers;
		        TextView _listOfAgents;
		
		        Button _btnInteractions;
		        Button _btnAddAgent;
		
		        EditText _inputAgentNumber;
		        Spinner _maxConcurrentCallSpinner;
		        protected override void OnCreate(Bundle savedInstanceState)
		        {
		            base.OnCreate(savedInstanceState);
		            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
		            SetContentView(Resource.Layout.activity_dialing);
		
		            _registered = false;
		            InitSoftphone();
		            Register();
		
		            _maxConcurrentCall = 0;
		
		            _status = FindViewById<TextView>(Resource.Id.status);
		
		            _listOfNumbers = FindViewById<TextView>(Resource.Id.listOfNumbers);
		            _listOfNumbers.MovementMethod = new ScrollingMovementMethod();
		
		            _listOfAgents = FindViewById<TextView>(Resource.Id.listOfAgents);
		            _listOfAgents.MovementMethod = new ScrollingMovementMethod();
		
		            _maxConcurrentCallSpinner = FindViewById<Spinner>(Resource.Id.maxConcurrentCallSpinner);
		            _btnAddAgent = FindViewById<Button>(Resource.Id.btnAddAgent);
		            _inputAgentNumber = FindViewById<EditText>(Resource.Id.inputAgentNumber);
		
		            ReadCSV();
		            LoadNumbers();
		
		            _btnLogout = FindViewById<TextView>(Resource.Id.btnLogOut);
		            _btnLogout.Click += delegate
		            {
		                if (_registered)
		                {
		                    _mySoftphone.Unregister();
		                }
		                Xamarin.Essentials.Preferences.Clear();
		                StartActivity(typeof(MainActivity));
		            };
		
		            _btnInteractions = FindViewById<Button>(Resource.Id.btnInteractions);
		            _btnInteractions.Click += delegate
		            {
		                if (_registered)
		                {
		                    StartAutodialer();
		                }
		            };
		
		            _btnAddAgent.Click += delegate
		            {
		                if (_inputAgentNumber.Text != "")
		                {
		                    AddAgent(_inputAgentNumber.Text);
		                    _listOfAgents.Text += String.Format("{0}\n", _inputAgentNumber.Text);
		                    _inputAgentNumber.Text = "";
		                }
		            };
		        }
		
		        void InitSoftphone()
		        {
		            _configStore = ConfigStore.Instance;
		            _mySoftphone = new Softphone();
		            _mySoftphone.PhoneLineStateChanged += mySoftphone_PhoneLineStateChanged;
		            _filename = "ExampleCSV.csv";
		            _callList = new List<CallInfo>();
		        }
		
		        void ReadCSV()
		        {
		            try
		            {
		                AssetManager assets = Application.Context.Assets;
		                using (StreamReader reader = new StreamReader(assets.Open(_filename)))
		                {
		                    string line;
		                    while ((line = reader.ReadLine()) != null)
		                    {
		                        ParseCSVLineToObjectList(line);
		                    }
		                }
		            }
		            catch (Exception ex)
		            {
		                Console.WriteLine("Error occured: {0}", ex.Message);
		            }
		        }
		
		        void ParseCSVLineToObjectList(string line)
		        {
		            try
		            {
		                string[] parse = line.Split(',', ';');
		                _callInfo = new CallInfo(parse[0], parse[1]);
		                _callList.Add(_callInfo);
		            }
		            catch (Exception ex)
		            {
		                Console.WriteLine("Error occured: {0}", ex.Message);
		            }
		        }
		
		        void LoadNumbers()
		        {
		            StringBuilder listOfNumbers = new StringBuilder();
		            foreach (var callListMember in _callList)
		            {
		                listOfNumbers.Append(String.Format("Phone number: \"{0}\", message: \"{1}\".\n", callListMember.PhoneNumber, callListMember.Message));
		            }
		            _listOfNumbers.Text = listOfNumbers.ToString();
		        }
		
		        void StartAutodialer()
		        {
		            _maxConcurrentCall = Int32.Parse(_maxConcurrentCallSpinner.SelectedItem.ToString());
		            _autoDialer = new Autodialer(_mySoftphone, _callList, _maxConcurrentCall);
		            _autoDialer.Start();
		        }
		
		        void AddAgent(string member)
		        {
		            _configStore.AddAgent(member);
		            _configStore.AddFreeAgent(member);
		        }
		
		        private void mySoftphone_PhoneLineStateChanged(object sender, RegistrationStateChangedArgs e)
		        {
		            if (e.State == RegState.Error || e.State == RegState.NotRegistered)
		            {
		                _registered = false;
		                _status.Text = "Not registered";
		            }
		            else if (e.State == RegState.RegistrationSucceeded)
		            {
		                _registered = true;
		                _status.Text = "Registered";
		            }
		        }
		
		        private void Register()
		        {
		            bool registrationRequired = true;
		            string displayName = Xamarin.Essentials.Preferences.Get("display_name", "");
		            string userName = Xamarin.Essentials.Preferences.Get("user_name", "");
		            string authenticationId = Xamarin.Essentials.Preferences.Get("authentication_id", "");
		            string registerPassword = Xamarin.Essentials.Preferences.Get("register_password", "");
		            string domainHost = Xamarin.Essentials.Preferences.Get("domain_host", "");
		            int domainPort = Xamarin.Essentials.Preferences.Get("domain_port", 5060);
		
		            _mySoftphone.Register(registrationRequired, displayName, userName, authenticationId, registerPassword,
		                domainHost, domainPort);
		        }
		
		        public override void OnBackPressed()
		        {
		            return;
		        }
		    }
		}
	

Code 10 - DialingActivity.cs

Creating the ExampleCSV.csv file

This csv file is going to store all the numbers with the messages we want to play to the recipient using the text to speech feature of the Ozeki VoIP SIP Android SDK. In the following video I'll show you how to create a new .csv file and include it in your Assets folder.

Video 10 - Creating the ExampleCSV.csv file

ExampleCSV.csv

1002;Hello world 2!
1003;Hello world 3!
	

Code 7 - ExampleCSV.csv

Running the example application

After the softphone is ready we can start debugging the application on an Android device with a version that's higher than Android 8.0. If we don't have an android device we can create an android emulator in the Visual Studio Community. To run the example application you'll need at least 3 phone lines to be registered. You can use the example softphone of the Windows VoIP SIP SDK, to test your android application.

Video 11 - Running the PredictiveDialer example application

Figure 5 - The example application

Conculsion

From this example you could learn how to create predictive autodialer, which is a softphone application and is able to read and process csv files, make calls simultaneously to the destinations, handle a ring group of agents, and send audio data from one call to another through itself by connecting the correct devices.