Android softphone: Simultaneous calls
In the following tutorial you will learn, how to make simultaneous calls using the Ozeki Adnroid SDK. Let's jump in and get started with the project.
Related: VoIP SIP managing multiple calls. If also you wish to build VoIP softphone on Windows, you might also be interested in a similar document, the VoIP SIP Multiple Calls in Ozeki VoIP SDK for Windows
Quick steps
- Download and install the Ozeki Andorid SDK
- Create a new Xamarin Android application in Viusal Studio
- Add the OzekiSDK.dll as a reference to our project
- Create the layout of the application
- Create a Softphone class that implements the ISoftphone interface
- Create the CallHandler class that will be used for handling the call events
- Create the MainActivity.cs activity which will ask for the login details
- Create the DialingActivity.cs file which will be responsible for receiving calls.
- Run the example application and receive multiple calls in the same time.
The 07_Simultaneous_Calls.zip file
You can download the 07_Simultaneous_Calls 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: 07_Simultaneous_Calls.zip (0Mb)
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.
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.
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 modified the layout of the Android softphone: Make calls project.
You can simply drag and drop these .xml files into you layout folder,
or you can create your own layout.
Download: layout.zip (1.91Kb)
<?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>
<?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="8" 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="Simultaneous calls"/> <TextView android:id="@+id/status" android:layout_width="250dp" android:layout_height="50dp" android:textAlignment="center" android:layout_row="2" android:layout_column="0" android:layout_gravity="center_vertical|center_horizontal" android:text="Not registered" /> <TextView android:id="@+id/inputNumberToCall" android:layout_width="300dp" android:layout_height="wrap_content" android:layout_marginTop="100dp" android:textAlignment="center" android:layout_row="3" android:textSize="18pt" android:layout_column="0" android:layout_gravity="center_vertical|center_horizontal" /> <GridLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:layout_gravity="center_vertical|center_horizontal" android:layout_row="5" android:layout_column="0" android:rowCount="5" android:columnCount="3" > <Button android:id="@+id/btnOne" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="1" android:layout_row="0" android:layout_margin="2dp" android:layout_column="0" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnTwo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="2" android:layout_row="0" android:layout_margin="2dp" android:layout_column="1" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnThree" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="3" android:layout_row="0" android:layout_margin="2dp" android:layout_column="2" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnFour" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="4" android:layout_row="1" android:layout_margin="2dp" android:layout_column="0" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnFive" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="5" android:layout_row="1" android:layout_margin="2dp" android:layout_column="1" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnSix" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="6" android:layout_row="1" android:layout_margin="2dp" android:layout_column="2" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnSeven" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="7" android:layout_row="2" android:layout_margin="2dp" android:layout_column="0" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnEight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="8" android:layout_row="2" android:layout_margin="2dp" android:layout_column="1" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnNine" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="9" android:layout_row="2" android:layout_margin="2dp" android:layout_column="2" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnStar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="*" android:layout_row="3" android:layout_margin="2dp" android:layout_column="0" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnZero" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0" android:layout_row="3" android:layout_margin="2dp" android:layout_column="1" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> <Button android:id="@+id/btnHashtag" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="#" android:layout_row="3" android:layout_margin="2dp" android:layout_column="2" android:backgroundTint="@android:color/holo_red_light" android:textColor="@android:color/white" android:state_enabled="false" /> </GridLayout> <Button android:id="@+id/btnInteractions" android:layout_width="250dp" android:layout_height="wrap_content" android:layout_row="6" 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" /> <TextView android:id="@+id/btnClear" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|center_horizontal" android:layout_row="7" android:layout_column="0" android:text="Clear" android:state_enabled="false" /> </GridLayout> </RelativeLayout>
Create the Softphone.cs file
With the softphone class, you can initialize a sofphone instance which will be responsible for receiving calls. In the following video you can see how to create the Softphone class.
Softphone.cs
using System;
using Ozeki.VoIP;
using Ozeki.Media;
namespace _07_Simultaneous_Calls
{
class Softphone
{
ISoftPhone _softphone;
IPhoneLine _phoneLine;
public event EventHandler<VoIPEventArgs<IPhoneCall>> IncomingCall;
public Softphone()
{
_softphone = SoftPhoneFactory.CreateSoftPhone(5000, 10000);
_softphone.IncomingCall += softphone_IncomingCall;
}
void softphone_IncomingCall(object sender, VoIPEventArgs<IPhoneCall> e)
{
var handler = IncomingCall;
if (handler != null) handler(this, e);
}
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" + ex.ToString());
}
}
private void phoneLine_RegistrationStateChanged(object sender,
RegistrationStateChangedArgs e)
{
var handler = PhoneLineStateChanged;
if (handler != null)
handler(this, e);
}
public event EventHandler<RegistrationStateChangedArgs> PhoneLineStateChanged;
}
}
Create the CallHandler.cs file
With the CallHandler class, you can initialize a CallHandler instance which will make easier for us to collect the ongoing calls in a list and interact with them. In the following video you can see how to create the CallHandler class.
CallHandler.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.
MainActivty.cs
using System;
using Android.App;
using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.AppCompat.App;
using Android.Widget;
namespace _07_Simultaneous_Calls
{
[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);
}
}
}
Create the DialingActivity.cs file
This activity will be responsible for making calls with codecs. With the dropdown menu you can select the right encoding for you, and the softphopne will use it for the call. In the following video I'll show you how to create the DialingActivity activity.
DialingActivty.cs
using Android.App;
using Android.OS;
using Android.Widget;
using Ozeki.Media;
using Ozeki.VoIP;
using System;
using System.Collections.Generic;
namespace _07_Simultaneous_Calls
{
[Activity(Label = "DialingActivity")]
public class DialingActivity : Activity
{
Softphone _mySoftphone;
static List<CallHandler> _myCallHandlers;
TextView _status;
TextView _btnLogout;
bool _registered;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
SetContentView(Resource.Layout.activity_dialing);
_mySoftphone = new Softphone();
_mySoftphone.PhoneLineStateChanged += mySoftphone_PhoneLineStateChanged;
_mySoftphone.IncomingCall += mySoftphone_IncomingCall;
_myCallHandlers = new List<CallHandler>();
_registered = false;
Register();
_status = FindViewById<TextView>(Resource.Id.status);
_btnLogout = FindViewById<TextView>(Resource.Id.btnLogOut);
_btnLogout.Click += delegate {
Xamarin.Essentials.Preferences.Clear();
StartActivity(typeof(MainActivity));
};
}
/*
This method will be called when there's an incoming call, and
our softphone will answer the call automaticaly.
*/
void mySoftphone_IncomingCall(object sender, VoIPEventArgs<IPhoneCall> phoneCall)
{
var callHandler = new CallHandler(phoneCall.Item);
callHandler.Completed += myCallHandler_Completed;
lock (_myCallHandlers)
_myCallHandlers.Add(callHandler);
callHandler.Start();
_status.Text = "In call";
}
/*
This method will be called when the caller hangs up the phone.
*/
void myCallHandler_Completed(object sender, EventArgs args)
{
lock (_myCallHandlers)
_myCallHandlers.Remove((CallHandler)sender);
if (_myCallHandlers.Count == 0)
{
if (_registered != false)
{
_status.Text = "Registered";
}
else
{
_status.Text = "Not registered";
}
}
else
{
_status.Text = "In call";
}
}
/*
Everytime the PhoneLineState changes this method will test, what is the
current PhoneLineState.
*/
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";
}
}
/*
The Register method will issue a SIP registration to the Ozeki Phone
System.
*/
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);
}
/*
This function overwrites the OnBackPressed() method of the class
so if you press the back button, you will not be able to go back to
the previous page. The only way you can go back to the registration
page is if you press the logout button.
*/
public override void OnBackPressed()
{
return;
}
}
}
Running the Softphone 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. In the following video you will also see, how to allow microphone for your application, and how to test if the codec is working.