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>
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:
_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.
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)
}
}
}
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 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.
}