Android

I. Setting up Android Studio project

Step 1: Download portsipSDK here .

Step 2 : Unzip the downloaded .zip file, copy the fileAndroid projectAndroidSample/SIPSample_AndroidStudio/SIPSample/libs/portsipvoipsdk.jarfolderapp/libs

Step 3 : Compile Libs: Right click on the fileportsipvoipsdk.jarand selectAdd as Library...

Step 4 : Declare the necessary permissions in the fileAndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">

  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
  <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
  <!-- Các permission liên quan đến internet. -->
  
  <uses-permission android:name="android.permission.WAKE_LOCK" />
  <!-- Allows the application to unlock the phone when there is an incoming call. -->
  <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
  <!-- Allows the application to open fullscreen interface when receiving incoming call notification
when the phone is in inactive state (lock screen, kill app, not in application screen) -->
  <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
  <!-- Allows users to operate when the device is in locked state (press to accept/reject calls). -->
  <uses-permission android:name="android.permission.RECORD_AUDIO" />
  <!-- Allow use of microphone. -->
  <uses-permission android:name="android.permission.CAMERA" />
  <!-- Allow use of camera. -->
  <uses-permission android:name="android.permission.VIBRATE" />
  <!-- Allows the application to control the phone to vibrate when there is an incoming call. -->

</manifest>

Reference : https://www.portsip.com/docs/sdk/manual/portsip-voip-sdk-user-manual-android.pdf

The examples from section II onwards apply to two files: MainActivity.kt and PortsipService.kt

II. Initialize SDK

In filePortsipService.kt

  • Declare global variable val portsipSDK: PortSipSdk = PortSipSdk(). Used to call PortsipSDK APIs.

  • Class PortsipService phải implement interface: **OnPortSIPEvent .**

fun initialSDK() {
    portsipSDK.setOnPortSIPEvent(this)
    portsipSDK.CreateCallManager(application)
    
    portsipSDK.initialize(
      PortSipEnumDefine.ENUM_TRANSPORT_TCP, "0.0.0.0", 10002,
      PortSipEnumDefine.ENUM_LOG_LEVEL_NONE, "", 8, "PortSIP SDK for Android",
      0, 0, "", "", false, "")
    
    // những cấu hình liên quan đến Audio
    portsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_AUDIOCODEC_OPUS)
    portsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_AUDIOCODEC_G729)
    portsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_AUDIOCODEC_PCMA)
    portsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_AUDIOCODEC_PCMU)

    // những cấu hình liên quan đến Video
    portsipSDK.addVideoCodec(PortSipEnumDefine.ENUM_VIDEOCODEC_H264)
    portsipSDK.addVideoCodec(PortSipEnumDefine.ENUM_VIDEOCODEC_VP8)
    portsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_VIDEOCODEC_VP9)
    portsipSDK.setVideoBitrate(-1, 512)
    portsipSDK.setVideoFrameRate(-1, 20)
    portsipSDK.setVideoResolution(480, 640)
    portsipSDK.setVideoNackStatus(true)
        
    // có thể đặt InstanceId là 1 constant bất kì.
    portsipSDK.setInstanceId("PORTSIP_INSTANCE_ID")
    
    portsipSDK.setLicenseKey("PORTSIP_UC_LICENSE")

}

This function should be called together with extension login.

III. Extension login

First, in the file MainActivity.kt, we have to launch it PortsipServicefirst.

// need to save data about extensions (extension number, password, domain tenant, ...) to the device's storage.
// use SharedPreferences
// Because when killing the app, the data will be lost, so this information must be saved to storage.

// Explanation:
// extension number: (eg: 1234, 5678, ...)
// extension password: (eg: abcDefg123, xzyqerTT11, ...)
// domain tenant: (eg: voip.example.com, ...)

val sharedPref = getSharedPreferences("CallManager", MODE_PRIVATE) ?: return 0
    with(sharedPref.edit()) {
      putString(getString(R.string.call_manager_ps_extension), số máy nhánh)
      putString(getString(R.string.call_manager_ps_password), password)
      putString(getString(R.string.call_manager_ps_domain), domain tenant)
      apply()
    }

Intent(this, PortsipService::class.java).also { intent ->
  startService(intent)
}

In the file PortsipService.kt, after startServicestep 1 onStartCommandis PortsipServicerun, the extension login can be called at this time, implemented as the following example

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
  registerPortsip()
  return super.onStartCommand(intent, flags, startId)
}

fun registerPortsip() {
  
  val sharedPref = getSharedPreferences("CallManager", MODE_PRIVATE)
  val psExtension = sharedPref.getString(getString(R.string.call_manager_ps_extension), "")
  val psPassword = sharedPref.getString(getString(R.string.call_manager_ps_password), "")
  val psDomain = sharedPref.getString(getString(R.string.call_manager_ps_domain), "")
  val pushMessage = sharedPref.getString(getString(R.string.call_manager_push_message), "") ?: ""

  // addSipMessageHeader dùng cho việc nhận cuộc gọi khi ứng dụng đang không active
 // example: turn off screen, kill app, ...
  // will be presented in detail in section VI.
  if (pushMessage.isNotEmpty()) {
    portsipSDK.clearAddedSipMessageHeaders()
    portsipSDK.addSipMessageHeader(-1, "REGISTER", 1, "x-p-push", pushMessage)
  }

  portsipSDK.removeUser()
  var code = portsipSDK.setUser(
    psExtension, psExtension, psExtension, psPassword, psDomain, "sip.etelecom.vn", 5063,
    "", 0, "", 0)
  
  if (code != PortSipErrorcode.ECoreErrorNone) {
    return
  }
  
  portsipSDK.unRegisterServer()
  code = portsipSDK.registerServer(90, 0)
  
}

There should be a variable portsipRegisteredto save the status of whether the portip has been registered successfully or not. If the registration is successful, just call the API refreshRegistration(), no need to go through the 2 steps initialSDK()above registerPortsip().

portsipSDK.refreshRegistration(0)

If you want to re-register from the beginning, you must initialize the SDK again as in section II .

There are 2 events that will occur when logging in to an extension:

// Extension login successful
fun onRegisterSuccess(reason: String?, code: Int, sipMessage: String?) {}

// Extension login failed
fun onRegisterFailure(reason: String?, code: Int, sipMessage: String?) {}

IV. Log out of extension

In filePortsipService.kt

portsipSDK.removeUser()
portsipSDK.unRegisterServer()

V. Making an outgoing call

In filePortsipService.kt

  • To make a call :

_sessionID = portsipSDK.call(callee: String, sendSdp: Boolean, videoCall: Boolean)
// _sessionID is a global variable, when any call is created, there will be a session, this _sessionID variable will save the id of that session.

// callee is the phone number the user wants to call.
/ // sendSdp: whether to transmit the Session Description Protocol or not. Usually set to true
// videoCall: whether to make a video call or not.
  • When the call recipient accepts the call:

Event onInviteAnswered()occurs.

fun onInviteAnswered(
  sessionId: Long,
  callerDisplayName: String,
  caller: String,
  calleeDisplayName: String,
  callee: String,
  audioCodecNames: String,
  videoCodecNames: String,
  existsAudio: Boolean,
  existsVideo: Boolean,
  sipMessage: String
) {}

When the call recipient rejects the call/the call is not answered/or for some reason the call is unsuccessful:

event onInviteFailure()occurs

fun onInviteFailure(sessionId: Long, reason: String?, code: Int, sipMessage: String?) {}

Actively end the call:

portsipSDK.hangUp(sessionID: Long)

VI. Receive call (incoming call)

Mobile Push (push notification voip) needs to be applied so that the application can receive calls in inactive cases:

  • User not on application screen

  • Lock screen

  • Kill app

6.1. Set up mobile push

To use mobile push, you need to declare your Android application with the portsip system through the following steps:

  • Get 2 information: 1 Server Key and 1 Sender ID on Firebase of Project.

  • applicationIdlocated in the build.gradle (Module: android.app)example file: vn.etelecom.app

  • Send this information to eTelecom 3 to create additional Mobile Push.

6.2. Reality

  • When there is a call to the extension :

Event onInviteIncoming()occurs.

onInviteIncoming(
  sessionId: Long,
  callerDisplayName: String,
  caller: String,
  calleeDisplayName: String,
  callee: String,
  audioCodecs: String,
  videoCodecs: String,
  existsAudio: Boolean,
  existsVideo: Boolean,
  sipMessage: String
) {
// do something...
// Example:
// start 1 Activity that displays incoming call information
}
  • Create a service called FirebaseService, and declare it inAndroidManifest.xml

FirebaseService.kt

class FirebaseService : FirebaseMessagingService() {

  companion object {
    const val ACTION_REFRESH_PUSH_TOKEN = "REFRESH_PUSH_TOKEN"
    const val ACTION_PROCESS_PUSH_PBX = "PROCESS_PUSH_PBX"
    const val PUSH_TOKEN = "PUSH_TOKEN"
  }

  override fun onCreate() {
    super.onCreate()
  }

  // When eTelecom sends a notification about a call to your extension.
  override fun onMessageReceived(remoteMessage: RemoteMessage) {
    // This data variable will include information about the caller, callee, whether there is a video call or not, ...
    val data: Map<String, String> = remoteMessage.data

    if ***app is in inactive state (kill app, screen off, ...)*** {

      Intent(this, PortsipService::class.java).also { intent ->
        intent.action = ACTION_PROCESS_PUSH_PBX
        startService(intent)
      }

    }
  }

  // When Firebase token is reset
  override fun onNewToken(newToken: String) {
    Intent(this, PortsipService::class.java).also { intent ->
      intent.action = ACTION_REFRESH_PUSH_TOKEN
      intent.putExtra(PUSH_TOKEN, newToken)
      startService(intent)
    }
  }

}

AndroidManifest.xml

<service android:name=".FirebaseService" android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
</service>
  • build.gradle(Project: android), add the following classpath:

buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.3.8'
    }
}
  • build.gradle(Module: android.app), add dependencies as follows:

apply plugin: 'com.google.gms.google-services'

dependencies {
  implementation platform('com.google.firebase:firebase-bom:28.1.0')
  implementation 'com.google.firebase:firebase-messaging'
}

Create notification channel: to use for notification of incoming calls. We should call this function at the same time as logging in to the extension.

fun initNotification() {
  notiManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

  val callChannel = NotificationChannel(BACKGROUND_INCOMING_CALL_CHANNEL_ID, "Cuộc gọi đến", NotificationManager.IMPORTANCE_HIGH)
  callChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
  
  notiManager?.createNotificationChannel(callChannel)  
}

When onStartCommandthe operation PortsipServiceis run, it is detailed in section III.

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
  if (intent != null) {
// there are 2 more situations to handle below (besides the situation presented in section III.)
// 1. When the Firebase token is refreshed
// 2. When the app is killed, or the phone screen is turned off, the incoming call will be sent via Firebase.
  }
    
  return super.onStartCommand(intent, flags, startId)
}
  • When the Firebase token is refreshed:

if (intent.action == FirebaseService.ACTION_REFRESH_PUSH_TOKEN) {
  firebasePushToken = intent.getStringExtra(FirebaseService.PUSH_TOKEN) ?: ""
  refreshPushToken(true)
  portsipSDK.refreshRegistration(0)
}
  • When you kill the app, or turn off the phone screen, incoming calls will be fired through Firebase and will be processed here:

if (intent.action == FirebaseService.ACTION_PROCESS_PUSH_PBX) {

   // Log in to the extension again, because when you kill the app, the login status will be lost.
    registerPortsip()
   // Then display popup notification of incoming call.
    showPendingCallNotification()

}
  • When starting the application, the first time we start PortsipService, we have to listen for the Firebase token and then log in to the extension.

if (intent?.action == null || intent.action!!.isEmpty()) {
  FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
    if (!task.isSuccessful) {
      Log.w("FIREBASE", "Fetching FCM registration token failed", task.exception)
      return@OnCompleteListener
    }

    // Get new FCM registration token
    firebasePushToken = task.result ?: ""
    refreshPushToken(true)
   // In case Firebase init is slow, which happens after successful extension login, we will need to refreshRegistration again.
    portsipSDK.refreshRegistration(0)
  })

  registerPortsip()
}
  • The function refreshPushTokencan be implemented as follows:

fun refreshPushToken(willPush: Boolean) {
  if (firebasePushToken.isNotEmpty()) {
    portsipSDK.clearAddedSipMessageHeaders()
    val pushMessage = "device-os=android;device-uid=$firebasePushToken;allow-call-push=$willPush;allow-message-push=$willPush;app-id=$packageName"
    portsipSDK.addSipMessageHeader(-1, "REGISTER", 1, "x-p-push", pushMessage)

   // save pushMessage to device storage
    val sharedPref = getSharedPreferences("CallManager", MODE_PRIVATE)
    with(sharedPref.edit()) {
      putString(getString(R.string.call_manager_push_message), pushMessage)
      apply()
    }
  }
}
  • The function showPendingNotificationcan be implemented as follows:

fun showPendingCallNotification() {
    
  // Cần phải có 1 Activity để hiển thị view Cuộc gọi đến 
  // khi user bấm vào notification hoặc khi màn hình tắt.
  val fullScreenIntent = Intent(this, IncomingActivity::class.java)
  fullScreenIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK

  val fullScreenPendingIntent = PendingIntent.getActivity(this, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)
  
  val notification = Notification.Builder(this, BACKGROUND_INCOMING_CALL_CHANNEL_ID)
    .setSmallIcon(R.drawable.ic_notification)
    .setColor(Color.argb(1, 47, 204, 112))
    .setContentTitle("Cuộc gọi đến từ")
    .setContentText(Html.fromHtml("<strong>${CallManager.callerParsed}</strong>", Html.FROM_HTML_MODE_LEGACY))
    .setOngoing(true)
    .setShowWhen(true)
    .setContentIntent(fullScreenPendingIntent)
    .setFullScreenIntent(fullScreenPendingIntent, true)
    .build()
  
  notiManager?.notify(BACKGROUND_INCOMING_CALL_NOTIFICATION, notification)
  
}

Some actions the user can perform when receiving a call: Accept (pick up the phone), Reject, Hang up (after accepting), etc...

//1. Accept the call
portsipSDK.answerCall(sessionId: Long, videoCall: Boolean) {}

//2. Reject the call
// code should be 486
portsipSDK.rejectCall(sessionId: Long, code: Int) {}

//3. Hang up after the call has been accepted and is in progress
portsipSDK.hangUp(sessionId: Long) {}

VII. Video call

There needs to be an Activity that represents the Video call, for example:

class VideoCallActivity: Activity(), View.OnClickListener {
   // user's video
  var localVideo
  // video of the person talking to the user
  var remoteVideo

 // when video call is initiated (1 of the caller or callee accepts call from the other)
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
  
    // the interface of this Activity, implemented in the file video_call_view.xml
    setContentView(R.layout.video_call_view)

    
    localVideo = findViewById(R.id.local_video_view)
    remoteVideo = findViewById(R.id.remote_video_view)

    portsipSDK.setRemoteVideoWindow(sessionId: Long, remoteVideo)
    portsipSDK.displayLocalVideo(true, true, localVideo)
    
   // if the user turns off the cam, the send parameter is false, otherwise it is true
    portsipSDK.sendVideo(sessionId: Long, send: Boolean)
  }

  // when the call ends, the Activity is dismissed, then these videos need to be released.
  override fun onDestroy() {
    super.onDestroy()

    portsipSDK.displayLocalVideo(false, false, null)
    localVideo.release()

    portsipSDK.setRemoteVideoWindow(sessionId: Long, null)
    remoteVideo.release()
  }
}

You should use the portsipSDK variable in the file PortsipService.ktto avoid creating multiple instances of a variable, which will cause many unwanted bugs.

The interface of the above Activity can be implemented in a file video_call_view.xmlas shown in the following example:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:orientation="vertical"
        android:background="#EEEEEE"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

  <!-- Remote Video: video of person talking to user -->
  <LinearLayout
            android:id="@+id/remote_video_view_wrapper"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:background="#333"
            android:orientation="vertical">

        <com.portsip.PortSIPVideoRenderer
                android:id="@+id/remote_video_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
  </LinearLayout>

  <!-- Local Video: user's video -->
  <LinearLayout
            android:id="@+id/local_video_view_wrapper"
            android:layout_width="120dp"
            android:layout_height="166dp"
            android:layout_alignParentTop="true"
            android:layout_marginTop="70dp"
            android:layout_alignParentEnd="true"
            android:layout_marginEnd="22dp"
            android:clipChildren="true"
            android:background="@drawable/transparent_rounded_corner">

        <com.portsip.PortSIPVideoRenderer
                android:id="@+id/local_video_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
  </LinearLayout>

</FrameLayout>

Make and receive calls :

// 1. Call out, with videoCall equal to true
portsipSDK.call(callee: String, true, videoCall: Boolean)

// 2. Receive incoming calls
onInviteIncoming(
  sessionId: Long,
  callerDisplayName: String,
  caller: String,
  calleeDisplayName: String,
  callee: String,
  audioCodecs: String,
  videoCodecs: String,
  existsAudio: Boolean,
  existsVideo: Boolean,
  sipMessage: String
) {
// existsVideo to indicate whether the current call is a videoCall or not.
}

Last updated