From 658cecc41acb9bc9fdabbe558b7e9277542c1b36 Mon Sep 17 00:00:00 2001 From: Isaac <134492608+isaacakakpo1@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:20:01 +0000 Subject: [PATCH] Release 1.3.7 (#346) * Version Bump * Main Stash * Update build.gradle * Implement WebRTC Stats * Update WebRTC Stats to Start when enabled * Update WebRTC Stats to Start when enabled * Implement peer Stop * Push Notifications Fix * - Update ReadMe - Refactor TelnyxClient * - Post `telnyx_rtc.media` to livedata - Remove parcelise as it's not supported by Java * - Update isDev Logic * - Refactor Code format * - Fix UnitTest Failures * Push Notifications Update * Push Notifications Update * Push Notifications Update --- README.md | 51 +++ app/src/main/AndroidManifest.xml | 17 +- .../main/java/com/telnyx/webrtc/sdk/App.kt | 1 + .../telnyx/webrtc/sdk/NotificationsService.kt | 175 ++++++++ .../com/telnyx/webrtc/sdk/di/AppModule.kt | 2 +- .../webrtc/sdk/ui/CallInstanceFragment.kt | 6 + .../com/telnyx/webrtc/sdk/ui/MainActivity.kt | 375 +++++++++++------- .../com/telnyx/webrtc/sdk/ui/MainViewModel.kt | 30 +- .../sdk/utility/MyFirebaseMessagingService.kt | 94 +---- app/src/test/CredentialTest.kt | 12 + .../main/java/com/telnyx/webrtc/sdk/Call.kt | 23 +- .../com/telnyx/webrtc/sdk/TelnyxClient.kt | 219 +++++++++- .../com/telnyx/webrtc/sdk/TelnyxConfig.kt | 2 +- .../telnyx/webrtc/sdk/model/PushMetaData.kt | 8 +- .../java/com/telnyx/webrtc/sdk/peer/Peer.kt | 96 ++++- .../com/telnyx/webrtc/sdk/socket/TxSocket.kt | 9 +- .../sdk/verto/receive/ReceivedResult.kt | 71 +--- .../sdk/verto/receive/SocketResponse.kt | 10 +- .../webrtc/sdk/verto/send/ParamRequest.kt | 26 ++ .../com/telnyx/webrtc/sdk/TelnyxClientTest.kt | 20 +- .../webrtc/sdk/testhelpers/TestConstants.kt | 2 +- 21 files changed, 903 insertions(+), 346 deletions(-) create mode 100644 app/src/main/java/com/telnyx/webrtc/sdk/NotificationsService.kt create mode 100644 app/src/test/CredentialTest.kt diff --git a/README.md b/README.md index fa9efe85..750eded8 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,14 @@ We can then use this method to create a listener that listens for an invitation SocketMethod.BYE.methodName -> { // Handle a call rejection or ending - Update UI or Navigate to new screen, etc. } + SocketMethod.RINGING.methodName -> { + // Client Can simulate ringing state + } + + SocketMethod.RINGING.methodName -> { + // Ringback tone is streamed to the caller + // early Media - Client Can simulate ringing state + } } } @@ -239,6 +247,49 @@ The `txPushMetaData` is neccessary for push notifications to work. For a detailed tutorial, please visit our official [Push Notification Docs](https://developers.telnyx.com/docs/voice/webrtc/push-notifications) +## Best Practices + + 1. Handling Push Notifications : In order to properly handle push notifications, we recommend using a call type (Foreground Service)[https://developer.android.com/develop/background-work/services/foreground-services] + with broadcast receiver to show push notifications. An answer or reject call intent with `telnyxPushMetaData` can then be passed to the MainActivity for processing. + - Play a ringtone when a call is received from push notification using the `RingtoneManager` + ``` kotlin + val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + RingtoneManager.getRingtone(applicationContext, notification).play() + ``` + - Make Sure to set these flags for your pendingIntents, so the values get updated anytime when the notification is clicked + ``` kotlin + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ``` + + ### Android 14 Requirements + In order to receive push notifications on Android 14, you will need to add the following permissions to your AndroidManifest.xml file and request a few at runtime: + ``` xml + // Request this permission at runtime + + + // If you need to use foreground services, you will need to add the following permissions + + + + // Configure foregroundservice and set the foreground service type + // Remember to stopForegroundService when the call is answered or rejected + + ``` + 2. Handling Multiple Calls : The Telnyx WebRTC SDK allows for multiple calls to be handled at once. + You can use the callId to differentiate the calls. + ``` kotlin + import java.util.UUID + // Retrieve all calls from the TelnyxClient + val calls: Map = telnyxClient.calls + + // Retrieve a specific call by callId + val currentCall: Call? = calls[callId] + ``` + + ## ProGuard changes NOTE: diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5c294edf..2f17fecc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + @@ -13,6 +15,9 @@ + + + - + + + \ No newline at end of file diff --git a/app/src/main/java/com/telnyx/webrtc/sdk/App.kt b/app/src/main/java/com/telnyx/webrtc/sdk/App.kt index 555c6ad6..6d22b375 100644 --- a/app/src/main/java/com/telnyx/webrtc/sdk/App.kt +++ b/app/src/main/java/com/telnyx/webrtc/sdk/App.kt @@ -5,6 +5,7 @@ package com.telnyx.webrtc.sdk import android.app.Application +import android.content.Context import dagger.hilt.android.HiltAndroidApp import timber.log.Timber diff --git a/app/src/main/java/com/telnyx/webrtc/sdk/NotificationsService.kt b/app/src/main/java/com/telnyx/webrtc/sdk/NotificationsService.kt new file mode 100644 index 00000000..894c90b2 --- /dev/null +++ b/app/src/main/java/com/telnyx/webrtc/sdk/NotificationsService.kt @@ -0,0 +1,175 @@ +package com.telnyx.webrtc.sdk + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.content.res.Resources.NotFoundException +import android.graphics.Color +import android.media.Ringtone +import android.media.RingtoneManager +import android.net.Uri +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import com.google.gson.Gson +import com.telnyx.webrtc.sdk.di.AppModule +import com.telnyx.webrtc.sdk.model.PushMetaData +import com.telnyx.webrtc.sdk.ui.MainActivity +import com.telnyx.webrtc.sdk.utility.MyFirebaseMessagingService +import timber.log.Timber + + +class NotificationsService : Service() { + + + companion object { + private const val CHANNEL_ID = "PHONE_CALL_NOTIFICATION_CHANNEL" + private const val NOTIFICATION_ID = 1 + const val STOP_ACTION = "STOP_ACTION" + } + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + private var ringtone:Ringtone? = null + + private fun playPushRingTone() { + try { + val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + ringtone = RingtoneManager.getRingtone(applicationContext, notification) + ringtone?.play() + } catch (e: NotFoundException) { + Timber.e("playPushRingTone: $e") + } + } + + + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + + val stopAction = intent?.action + if (stopAction != null && stopAction == STOP_ACTION) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + ringtone?.stop() + } else { + stopForeground(true) + } + return START_NOT_STICKY + } + + val metadata = intent?.getStringExtra("metadata") + val telnyxPushMetadata = Gson().fromJson(metadata, PushMetaData::class.java) + telnyxPushMetadata?.let { + showNotification(it) + playPushRingTone() + + } + return START_STICKY + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Phone Call Notifications" + val description = "Notifications for incoming phone calls" + val importance = NotificationManager.IMPORTANCE_HIGH + val channel = NotificationChannel(CHANNEL_ID, name, importance) + channel.description = description + + val notificationManager = getSystemService(NotificationManager::class.java) + channel.apply { + lightColor = Color.RED + enableLights(true) + enableVibration(true) + setSound(null, null) + } + notificationManager.createNotificationChannel(channel) + } + } + + private fun showNotification(txPushMetaData: PushMetaData) { + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val pendingIntent: PendingIntent = + PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE) + + val customSoundUri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + + val rejectResultIntent = Intent(this, MainActivity::class.java) + rejectResultIntent.action = Intent.ACTION_VIEW + rejectResultIntent.putExtra( + MyFirebaseMessagingService.EXT_KEY_DO_ACTION, + MyFirebaseMessagingService.ACT_REJECT_CALL + ) + rejectResultIntent.putExtra( + MyFirebaseMessagingService.TX_PUSH_METADATA, + txPushMetaData.toJson() + ) + val rejectPendingIntent = PendingIntent.getActivity( + this, + MyFirebaseMessagingService.REJECT_REQUEST_CODE, + rejectResultIntent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + val answerResultIntent = Intent(this, MainActivity::class.java) + answerResultIntent.setAction(Intent.ACTION_VIEW) + + answerResultIntent.putExtra( + MyFirebaseMessagingService.EXT_KEY_DO_ACTION, + MyFirebaseMessagingService.ACT_ANSWER_CALL + ) + + answerResultIntent.putExtra( + MyFirebaseMessagingService.TX_PUSH_METADATA, + txPushMetaData.toJson() + ) + + val answerPendingIntent = PendingIntent.getActivity( + this, + MyFirebaseMessagingService.ANSWER_REQUEST_CODE, + answerResultIntent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + Timber.d("showNotification: ${txPushMetaData.toJson()}") + + + val builder = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_stat_contact_phone) + .setContentTitle("Incoming Call") + .setContentText("Incoming call from: ") + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentIntent(pendingIntent) + .setSound(customSoundUri) + .addAction( + R.drawable.ic_call_white, + MyFirebaseMessagingService.ACT_ANSWER_CALL, answerPendingIntent + ) + .addAction( + R.drawable.ic_call_end_white, + MyFirebaseMessagingService.ACT_REJECT_CALL, rejectPendingIntent + ) + .setOngoing(true) + .setAutoCancel(false) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setFullScreenIntent(pendingIntent, true) + + startForeground( + NOTIFICATION_ID, + builder.build(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + ) + } + + +} diff --git a/app/src/main/java/com/telnyx/webrtc/sdk/di/AppModule.kt b/app/src/main/java/com/telnyx/webrtc/sdk/di/AppModule.kt index 14f96efb..b046a268 100644 --- a/app/src/main/java/com/telnyx/webrtc/sdk/di/AppModule.kt +++ b/app/src/main/java/com/telnyx/webrtc/sdk/di/AppModule.kt @@ -17,7 +17,7 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object AppModule { - private const val SHARED_PREFERENCES_KEY = "TelnyxSharedPreferences" + const val SHARED_PREFERENCES_KEY = "TelnyxSharedPreferences" @Singleton @Provides diff --git a/app/src/main/java/com/telnyx/webrtc/sdk/ui/CallInstanceFragment.kt b/app/src/main/java/com/telnyx/webrtc/sdk/ui/CallInstanceFragment.kt index ee06e764..9682007c 100644 --- a/app/src/main/java/com/telnyx/webrtc/sdk/ui/CallInstanceFragment.kt +++ b/app/src/main/java/com/telnyx/webrtc/sdk/ui/CallInstanceFragment.kt @@ -146,6 +146,12 @@ class CallInstanceFragment : Fragment(), NumberKeyboardListener { when (data?.method) { SocketMethod.INVITE.methodName -> { //NOOP + } + SocketMethod.RINGING.methodName -> { + + } + SocketMethod.MEDIA.methodName -> { + } SocketMethod.BYE.methodName -> { diff --git a/app/src/main/java/com/telnyx/webrtc/sdk/ui/MainActivity.kt b/app/src/main/java/com/telnyx/webrtc/sdk/ui/MainActivity.kt index 84a2b479..2a74a2c7 100644 --- a/app/src/main/java/com/telnyx/webrtc/sdk/ui/MainActivity.kt +++ b/app/src/main/java/com/telnyx/webrtc/sdk/ui/MainActivity.kt @@ -9,9 +9,14 @@ import android.Manifest.permission.INTERNET import android.Manifest.permission.RECORD_AUDIO import android.app.AlertDialog import android.app.Dialog +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.content.Intent +import android.graphics.Color import android.media.RingtoneManager import android.os.Build import android.os.Bundle @@ -24,17 +29,20 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider import com.google.firebase.FirebaseApp import com.google.firebase.messaging.FirebaseMessaging +import com.google.gson.Gson import com.karumi.dexter.Dexter import com.karumi.dexter.MultiplePermissionsReport import com.karumi.dexter.PermissionToken import com.karumi.dexter.listener.PermissionRequest import com.karumi.dexter.listener.multi.MultiplePermissionsListener +import com.telnyx.webrtc.sdk.App import com.telnyx.webrtc.sdk.CredentialConfig import com.telnyx.webrtc.sdk.MOCK_CALLER_NAME import com.telnyx.webrtc.sdk.MOCK_CALLER_NUMBER import com.telnyx.webrtc.sdk.MOCK_DESTINATION_NUMBER import com.telnyx.webrtc.sdk.MOCK_PASSWORD import com.telnyx.webrtc.sdk.MOCK_USERNAME +import com.telnyx.webrtc.sdk.NotificationsService import com.telnyx.webrtc.sdk.R import com.telnyx.webrtc.sdk.TokenConfig import com.telnyx.webrtc.sdk.databinding.ActivityMainBinding @@ -42,6 +50,7 @@ import com.telnyx.webrtc.sdk.manager.UserManager import com.telnyx.webrtc.sdk.model.AudioDevice import com.telnyx.webrtc.sdk.model.CallState import com.telnyx.webrtc.sdk.model.LogLevel +import com.telnyx.webrtc.sdk.model.PushMetaData import com.telnyx.webrtc.sdk.model.SocketMethod import com.telnyx.webrtc.sdk.model.TxServerConfiguration import com.telnyx.webrtc.sdk.ui.wsmessages.WsMessageFragment @@ -77,6 +86,8 @@ class MainActivity : AppCompatActivity() { private var isDev = false private var isAutomaticLogin = false private var wsMessageList: ArrayList? = null + private var credentialConfig: CredentialConfig? = null + private var tokenConfig: TokenConfig? = null // Notification handling private var notificationAcceptHandling: Boolean? = null @@ -110,8 +121,10 @@ class MainActivity : AppCompatActivity() { checkPermissions() - handleCallNotification() initViews() + handleServiceIntent(intent) + handleUserLoginState() + binding.toolbarId.setOnMenuItemClickListener(this::onOptionsItemSelected) } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -121,10 +134,9 @@ class MainActivity : AppCompatActivity() { } - - override fun onOptionsItemSelected(item: MenuItem):Boolean { + override fun onOptionsItemSelected(item: MenuItem): Boolean { Timber.d("onOptionsItemSelected ${item.itemId}") - return when (item.itemId) { + return when (item.itemId) { R.id.action_disconnect -> { if (userManager.isUserLogin) { disconnectPressed() @@ -159,6 +171,7 @@ class MainActivity : AppCompatActivity() { } } } + private fun createAudioOutputSelectionDialog(): Dialog { return this.let { val audioOutputList = arrayOf("Phone", "Bluetooth", "Loud Speaker") @@ -194,112 +207,192 @@ class MainActivity : AppCompatActivity() { } private fun connectToSocketAndObserve(txPushMetaData: String? = null) { + + Timber.d("doLogin") + // path to ringtone and ringBackTone + val ringtone = R.raw.incoming_call + val ringBackTone = R.raw.ringback_tone + + if (userManager.isUserLogin) { + val loginConfig = CredentialConfig( + userManager.sipUsername, + userManager.sipPass, + userManager.callerIdNumber, + userManager.callerIdNumber, + userManager.fcmToken, + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),// or ringtone, + R.raw.ringback_tone, + LogLevel.ALL + ) + credentialConfig = loginConfig + } else { + binding.loginSectionId.apply { + if (tokenLoginSwitch.isChecked) { + loginTokenId.apply { + val sipToken = sipTokenId.text.toString() + val sipCallerName = tokenCallerIdNameId.text.toString() + val sipCallerNumber = tokenCallerIdNumberId.text.toString() + + val loginConfig = TokenConfig( + sipToken, + sipCallerName, + sipCallerNumber, + fcmToken, + ringtone, + ringBackTone, + LogLevel.ALL + ) + tokenConfig = loginConfig + } + } else { + loginCredentialId.apply { + val sipUsername = sipUsernameId.text.toString() + val password = sipPasswordId.text.toString() + val sipCallerName = callerIdNameId.text.toString() + val sipCallerNumber = callerIdNumberId.text.toString() + + val loginConfig = CredentialConfig( + sipUsername, + password, + sipCallerName, + sipCallerNumber, + fcmToken, + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE), // or ringtone, + ringBackTone, + LogLevel.ALL + ) + credentialConfig = loginConfig + } + } + } + + } + Timber.d("Connect to Socket and Observe") if (!isDev) { - mainViewModel.initConnection(applicationContext, null, txPushMetaData) + mainViewModel.initConnection( + applicationContext, + null, + credentialConfig = credentialConfig!!, + tokenConfig = tokenConfig, + txPushMetaData + ) } else { mainViewModel.initConnection( applicationContext, TxServerConfiguration(host = "rtcdev.telnyx.com"), + credentialConfig = credentialConfig, + tokenConfig = tokenConfig, txPushMetaData ) + } observeSocketResponses() } private fun observeSocketResponses() { - mainViewModel.getSocketResponse() - ?.observe( - this, - object : SocketObserver() { - override fun onConnectionEstablished() { - doLogin(isAutomaticLogin) - } + mainViewModel.getSocketResponse()?.observe( + this, + object : SocketObserver() { + override fun onConnectionEstablished() { + Timber.d("OnConMan") - override fun onMessageReceived(data: ReceivedMessageBody?) { - Timber.d("onMessageReceived from SDK [%s]", data?.method) - when (data?.method) { - SocketMethod.CLIENT_READY.methodName -> { - Timber.d("You are ready to make calls.") - } + } - SocketMethod.LOGIN.methodName -> { - binding.progressIndicatorId.visibility = View.INVISIBLE - val sessionId = (data.result as LoginResponse).sessid - Timber.d("Current Session: $sessionId") - onLoginSuccessfullyViews() - } + override fun onMessageReceived(data: ReceivedMessageBody?) { + Timber.d("onMessageReceived from SDK [%s]", data?.method) + when (data?.method) { + SocketMethod.CLIENT_READY.methodName -> { + Timber.d("You are ready to make calls.") - SocketMethod.INVITE.methodName -> { - val inviteResponse = data.result as InviteResponse - onReceiveCallView( - inviteResponse.callId, - inviteResponse.callerIdNumber - ) - } + } - SocketMethod.ANSWER.methodName -> { - val callId = (data.result as AnswerResponse).callId - launchCallInstance(callId) - binding.apply { - callControlSectionId.callButtonId.visibility = - View.VISIBLE - callControlSectionId.cancelCallButtonId.visibility = - View.GONE - } - - invitationSent = false - } + SocketMethod.LOGIN.methodName -> { + binding.progressIndicatorId.visibility = View.INVISIBLE + val sessionId = (data.result as LoginResponse).sessid + Timber.d("Current Session: $sessionId") + onLoginSuccessfullyViews() + } - SocketMethod.BYE.methodName -> { - onByeReceivedViews() - val callId = (data.result as ByeResponse).callId - val callInstanceFragment = callInstanceFragments[callId] - callInstanceFragment?.let { - supportFragmentManager.beginTransaction().remove(it).commit() - } + SocketMethod.INVITE.methodName -> { + val inviteResponse = data.result as InviteResponse + onReceiveCallView( + inviteResponse.callId, + inviteResponse.callerIdNumber + ) + } + + SocketMethod.ANSWER.methodName -> { + val callId = (data.result as AnswerResponse).callId + launchCallInstance(callId) + binding.apply { + callControlSectionId.callButtonId.visibility = + View.VISIBLE + callControlSectionId.cancelCallButtonId.visibility = + View.GONE } + + invitationSent = false } - } - override fun onLoading() { - Timber.i("Loading...") - } + SocketMethod.RINGING.methodName -> { + // Client Can simulate ringing state + } - override fun onChanged(value: SocketResponse) { - super.onChanged(value) - // Do Nothing - } + SocketMethod.MEDIA.methodName -> { + // Ringback tone is streamed to the caller + // early Media - Client Can simulate ringing state + } - override fun onError(message: String?) { - Timber.e("onError: %s", message) - Toast.makeText( - this@MainActivity, - message ?: "Socket Connection Error", - Toast.LENGTH_SHORT - ).show() + SocketMethod.BYE.methodName -> { + onByeReceivedViews() + val callId = (data.result as ByeResponse).callId + val callInstanceFragment = callInstanceFragments[callId] + callInstanceFragment?.let { + supportFragmentManager.beginTransaction().remove(it).commit() + } + } } + } - override fun onSocketDisconnect() { - Toast.makeText( - this@MainActivity, - "Socket is disconnected", - Toast.LENGTH_SHORT - ).show() - - binding.apply { - progressIndicatorId.visibility = View.INVISIBLE - incomingCallView.visibility = View.GONE - callControlView.visibility = View.GONE - loginSectionView.visibility = View.VISIBLE - - socketTextValue.text = getString(R.string.disconnected) - callStateTextValue.text = "-" - } + override fun onLoading() { + Timber.i("Loading...") + } + override fun onChanged(value: SocketResponse) { + super.onChanged(value) + // Do Nothing + } + + override fun onError(message: String?) { + Timber.e("onError: %s", message) + Toast.makeText( + this@MainActivity, + message ?: "Socket Connection Error", + Toast.LENGTH_SHORT + ).show() + } + override fun onSocketDisconnect() { + Toast.makeText( + this@MainActivity, + "Socket is disconnected", + Toast.LENGTH_SHORT + ).show() + + binding.apply { + progressIndicatorId.visibility = View.INVISIBLE + incomingCallView.visibility = View.GONE + callControlView.visibility = View.GONE + loginSectionView.visibility = View.VISIBLE + + socketTextValue.text = getString(R.string.disconnected) + callStateTextValue.text = "-" } + + } - ) + } + ) } private fun observeWsMessage() { @@ -323,7 +416,6 @@ class MainActivity : AppCompatActivity() { private fun initViews() { mockInputs() - handleUserLoginState() getFCMToken() observeWsMessage() @@ -466,75 +558,17 @@ class MainActivity : AppCompatActivity() { private fun connectButtonPressed() { binding.progressIndicatorId.visibility = View.VISIBLE - if (notificationAcceptHandling == true) { - Timber.d("notificationAcceptHandling is true $txPushMetaData") - if (txPushMetaData != null) { - connectToSocketAndObserve(txPushMetaData) - } - } else { + Timber.d("notificationAcceptHandling is true $txPushMetaData") + if (txPushMetaData != null) { + connectToSocketAndObserve(txPushMetaData) + } + else { connectToSocketAndObserve() } } private fun doLogin(isAuto: Boolean) { - // path to ringtone and ringBackTone - val ringtone = R.raw.incoming_call - val ringBackTone = R.raw.ringback_tone - - if (isAuto) { - val loginConfig = CredentialConfig( - userManager.sipUsername, - userManager.sipPass, - userManager.callerIdNumber, - userManager.callerIdNumber, - userManager.fcmToken, - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),// or ringtone, - R.raw.ringback_tone, - LogLevel.ALL - ) - mainViewModel.doLoginWithCredentials(loginConfig) - } else { - binding.loginSectionId.apply { - if (tokenLoginSwitch.isChecked) { - loginTokenId.apply { - val sipToken = sipTokenId.text.toString() - val sipCallerName = tokenCallerIdNameId.text.toString() - val sipCallerNumber = tokenCallerIdNumberId.text.toString() - val loginConfig = TokenConfig( - sipToken, - sipCallerName, - sipCallerNumber, - fcmToken, - ringtone, - ringBackTone, - LogLevel.ALL - ) - mainViewModel.doLoginWithToken(loginConfig) - } - } else { - loginCredentialId.apply { - val sipUsername = sipUsernameId.text.toString() - val password = sipPasswordId.text.toString() - val sipCallerName = callerIdNameId.text.toString() - val sipCallerNumber = callerIdNumberId.text.toString() - - val loginConfig = CredentialConfig( - sipUsername, - password, - sipCallerName, - sipCallerNumber, - fcmToken, - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE), // or ringtone, - ringBackTone, - LogLevel.ALL - ) - mainViewModel.doLoginWithCredentials(loginConfig) - } - } - } - - } } private fun getFCMToken() { @@ -655,12 +689,14 @@ class MainActivity : AppCompatActivity() { incomingActiveCallSectionId.root.bringToFront() incomingActiveCallSectionId.endAndAccept.setOnClickListener { - mainViewModel.currentCall?.let { + mainViewModel.currentCall!!.let { isActiveBye = true it.endCall(it.callId) + }.also { + mainViewModel.setCurrentCall(callId) + onAcceptCall(callId, callerIdNumber) } - mainViewModel.setCurrentCall(callId) - onAcceptCall(callId, callerIdNumber) + } incomingActiveCallSectionId.rejectCurrentCall.setOnClickListener { @@ -757,20 +793,59 @@ class MainActivity : AppCompatActivity() { } } - private fun handleCallNotification() { + private fun handleCallNotification(intent: Intent?) { + + if (intent == null) { + Timber.d("Intent is null") + return + } + + Timber.d("onNewIntent ") + val serviceIntent = Intent(this, NotificationsService::class.java).apply { + putExtra("action", NotificationsService.STOP_ACTION) + } + serviceIntent.setAction(NotificationsService.STOP_ACTION) + startService(serviceIntent) + val action = intent.extras?.getString(MyFirebaseMessagingService.EXT_KEY_DO_ACTION) action?.let { txPushMetaData = intent.extras?.getString(MyFirebaseMessagingService.TX_PUSH_METADATA) + Timber.d("Action: $action ${txPushMetaData ?: "No Metadata"}") if (action == MyFirebaseMessagingService.ACT_ANSWER_CALL) { // Handle Answer notificationAcceptHandling = true + Timber.d("Call answered from notification") + } else if (action == MyFirebaseMessagingService.ACT_REJECT_CALL) { // Handle Reject notificationAcceptHandling = false + Timber.d("Call rejected from notification") } + connectButtonPressed() } } + override fun onResume() { + super.onResume() + Timber.d("onResume") + } + + override fun onStop() { + super.onStop() + Timber.d("onStop") + disconnectPressed() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleServiceIntent(intent) + } + + private fun handleServiceIntent(intent: Intent?) { + + handleCallNotification(intent) + } + } diff --git a/app/src/main/java/com/telnyx/webrtc/sdk/ui/MainViewModel.kt b/app/src/main/java/com/telnyx/webrtc/sdk/ui/MainViewModel.kt index af20fab2..3dac62b1 100644 --- a/app/src/main/java/com/telnyx/webrtc/sdk/ui/MainViewModel.kt +++ b/app/src/main/java/com/telnyx/webrtc/sdk/ui/MainViewModel.kt @@ -40,16 +40,36 @@ class MainViewModel @Inject constructor( fun initConnection( context: Context, providedServerConfig: TxServerConfiguration?, + credentialConfig: CredentialConfig?, + tokenConfig: TokenConfig?, txPushMetaData: String? ) { + Timber.e("initConnection") telnyxClient = TelnyxClient(context) + providedServerConfig?.let { - telnyxClient?.connect(it, txPushMetaData) + telnyxClient?.connect(it, credentialConfig!!, txPushMetaData, true) } ?: run { - telnyxClient?.connect(txPushMetaData = txPushMetaData) + if (tokenConfig != null) { + telnyxClient?.connect( + txPushMetaData = txPushMetaData, + tokenConfig = tokenConfig, + autoLogin = true + ) + } else { + telnyxClient?.connect( + txPushMetaData = txPushMetaData, + credentialConfig = credentialConfig!!, + autoLogin = true + ) + } } } + fun startDebugStats() { + currentCall?.startDebug() + } + fun saveUserData( userName: String, password: String, @@ -89,10 +109,6 @@ class MainViewModel @Inject constructor( fun getIsOnHoldStatus(): LiveData? = currentCall?.getIsOnHoldStatus() fun getIsOnLoudSpeakerStatus(): LiveData? = currentCall?.getIsOnLoudSpeakerStatus() - fun doLoginWithCredentials(credentialConfig: CredentialConfig) { - telnyxClient?.credentialLogin(credentialConfig) - Timber.e("token_ ${credentialConfig.fcmToken}") - } fun doLoginWithToken(tokenConfig: TokenConfig) { telnyxClient?.tokenLogin(tokenConfig) @@ -104,7 +120,7 @@ class MainViewModel @Inject constructor( destinationNumber: String, clientState: String ) { - val call = telnyxClient?.newInvite( + val call = telnyxClient?.newInvite( callerName, callerNumber, destinationNumber, clientState, mapOf(Pair("X-test", "123456")) ) diff --git a/app/src/main/java/com/telnyx/webrtc/sdk/utility/MyFirebaseMessagingService.kt b/app/src/main/java/com/telnyx/webrtc/sdk/utility/MyFirebaseMessagingService.kt index 8c6759b4..0eb18462 100644 --- a/app/src/main/java/com/telnyx/webrtc/sdk/utility/MyFirebaseMessagingService.kt +++ b/app/src/main/java/com/telnyx/webrtc/sdk/utility/MyFirebaseMessagingService.kt @@ -4,25 +4,13 @@ package com.telnyx.webrtc.sdk.utility -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context import android.content.Intent -import android.graphics.Color -import android.media.RingtoneManager -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.google.gson.Gson -import com.telnyx.webrtc.sdk.R -import com.telnyx.webrtc.sdk.model.PushMetaData -import com.telnyx.webrtc.sdk.ui.MainActivity +import com.telnyx.webrtc.sdk.NotificationsService import org.json.JSONObject import timber.log.Timber -import java.util.* class MyFirebaseMessagingService : FirebaseMessagingService() { @@ -35,83 +23,19 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) Timber.d("Message Received From Firebase: ${remoteMessage.data}") + Timber.d("Message Received From Firebase Priority: ${remoteMessage.priority}") + Timber.d("Message Received From Firebase: ${remoteMessage.originalPriority}") val params = remoteMessage.data val objects = JSONObject(params as Map<*, *>) val metadata = objects.getString("metadata") - val gson = Gson() - val telnyxPushMetadata = gson.fromJson(metadata, PushMetaData::class.java) - - val notificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val notificationID = Random().nextInt(3000) - - /* - Apps targeting SDK 26 or above (Android O) must implement notification channels and add its notifications - to at least one of them. - */ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setupChannels(notificationManager) + val serviceIntent = Intent(this, NotificationsService::class.java).apply { + putExtra("metadata", metadata) } - - val rejectResultIntent = Intent(this, MainActivity::class.java) - rejectResultIntent.addCategory(Intent.CATEGORY_LAUNCHER) - rejectResultIntent.action = Intent.ACTION_VIEW - rejectResultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) - rejectResultIntent.putExtra(EXT_KEY_DO_ACTION, ACT_REJECT_CALL) - val rejectPendingIntent = PendingIntent.getActivity( - this, - REJECT_REQUEST_CODE, - rejectResultIntent, - PendingIntent.FLAG_IMMUTABLE - ) - - val answerResultIntent = Intent(this, MainActivity::class.java) - answerResultIntent.addCategory(Intent.CATEGORY_LAUNCHER) - answerResultIntent.action = Intent.ACTION_VIEW - answerResultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) - answerResultIntent.putExtra(EXT_KEY_DO_ACTION, ACT_ANSWER_CALL) - - answerResultIntent.putExtra(TX_PUSH_METADATA, metadata) - - val answerPendingIntent = PendingIntent.getActivity( - this, - ANSWER_REQUEST_CODE, - answerResultIntent, - PendingIntent.FLAG_IMMUTABLE - ) - - val notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - val notificationBuilder = NotificationCompat.Builder(this, TELNYX_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_stat_contact_phone) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setContentTitle(remoteMessage.data["title"]) - .setContentText(telnyxPushMetadata.callerName + " - " + telnyxPushMetadata.callerNumber) - .setVibrate(longArrayOf(1000, 1000, 1000, 1000, 1000)) - .addAction(R.drawable.ic_call_white, ACT_ANSWER_CALL, answerPendingIntent) - .addAction(R.drawable.ic_call_end_white, ACT_REJECT_CALL, rejectPendingIntent) - .setAutoCancel(true) - .setSound(notificationSoundUri) - - notificationManager.notify(notificationID, notificationBuilder.build()) + startForegroundService(serviceIntent) } - @RequiresApi(api = Build.VERSION_CODES.O) - private fun setupChannels(notificationManager: NotificationManager?) { - val adminChannelName = "New notification" - val adminChannelDescription = "Device to device notification" - val adminChannel = NotificationChannel( - TELNYX_CHANNEL_ID, - adminChannelName, - NotificationManager.IMPORTANCE_HIGH - ) - adminChannel.description = adminChannelDescription - adminChannel.enableLights(true) - adminChannel.lightColor = Color.RED - adminChannel.enableVibration(true) - notificationManager?.createNotificationChannel(adminChannel) - } /** * Called if InstanceID token is updated. This may occur if the security of @@ -142,9 +66,9 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { companion object { private const val TAG = "MyFirebaseMsgService" - private const val TELNYX_CHANNEL_ID = "telnyx_channel" - private const val ANSWER_REQUEST_CODE = 0 - private const val REJECT_REQUEST_CODE = 1 + const val TELNYX_CHANNEL_ID = "telnyx_channel" + const val ANSWER_REQUEST_CODE = 0 + const val REJECT_REQUEST_CODE = 1 const val TX_PUSH_METADATA = "tx_push_metadata" diff --git a/app/src/test/CredentialTest.kt b/app/src/test/CredentialTest.kt new file mode 100644 index 00000000..3bbc54e9 --- /dev/null +++ b/app/src/test/CredentialTest.kt @@ -0,0 +1,12 @@ +import com.telnyx.webrtc.sdk.MOCK_PASSWORD +import com.telnyx.webrtc.sdk.MOCK_USERNAME +import kotlin.test.Test +import kotlin.test.assertEquals + +class CredentialTest { + @Test + fun testCredential() { + assertEquals(MOCK_USERNAME, "") + assertEquals(MOCK_PASSWORD, "") + } +} diff --git a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/Call.kt b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/Call.kt index 3e05dd4c..153a66b2 100644 --- a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/Call.kt +++ b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/Call.kt @@ -41,23 +41,22 @@ import kotlin.concurrent.timerTask */ data class CustomHeaders(val name: String, val value: String) - -data class Call( + data class Call( val context: Context, val client: TelnyxClient, var socket: TxSocket, val sessionId: String, val audioManager: AudioManager, val providedTurn: String = Config.DEFAULT_TURN, - val providedStun: String = Config.DEFAULT_STUN -) { + val providedStun: String = Config.DEFAULT_STUN, + ) { companion object { const val ICE_CANDIDATE_DELAY: Long = 400 } - internal var peerConnection: Peer? = null + internal var earlySDP = false var inviteResponse:InviteResponse? = null @@ -86,6 +85,19 @@ data class Call( loudSpeakerLiveData.postValue(audioManager.isSpeakerphoneOn) } + fun startDebug(){ + Timber.d("Peer connection debug started") + + peerConnection?.startTimer() + } + + fun stopDebug(){ + Timber.d("Peer connection debug stopped") + peerConnection?.stopTimer() + } + + + /** * Initiates a new call invitation * @param callerName, the name to appear on the invitation @@ -131,6 +143,7 @@ data class Call( customHeaders: Map? = null ) { client.acceptCall(callId, destinationNumber, customHeaders) + } /** diff --git a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxClient.kt b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxClient.kt index 768bca7f..b0f5bfc3 100644 --- a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxClient.kt +++ b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxClient.kt @@ -61,6 +61,7 @@ class TelnyxClient( const val RETRY_REGISTER_TIME = 3 const val RETRY_CONNECT_TIME = 3 const val GATEWAY_RESPONSE_DELAY: Long = 3000 + } private var credentialSessionConfig: CredentialConfig? = null @@ -83,11 +84,13 @@ class TelnyxClient( internal var providedTurn: String? = null internal var providedStun: String? = null + internal var debugReportStarted = false + // MediaPlayer for ringtone / ringbacktone private var mediaPlayer: MediaPlayer? = null var sessid: String // sessid used to recover calls when reconnecting - val socketResponseLiveData = MutableLiveData>() + lateinit var socketResponseLiveData: MutableLiveData> val wsMessagesResponseLiveDate = MutableLiveData() private val audioManager = @@ -111,6 +114,7 @@ class TelnyxClient( private var isCallPendingFromPush: Boolean = false private var pushMetaData: PushMetaData? = null private fun processCallFromPush(metaData: PushMetaData) { + Log.d("processCallFromPush PushMetaData", metaData.toJson()) isCallPendingFromPush = true this.pushMetaData = metaData } @@ -130,7 +134,7 @@ class TelnyxClient( sessid, audioManager!!, providedTurn!!, - providedStun!! + providedStun!!, ) } } else { @@ -150,7 +154,7 @@ class TelnyxClient( callId: UUID, destinationNumber: String, customHeaders: Map? = null - ) : Call { + ): Call { val acceptCall = calls[callId] acceptCall!!.apply { val uuid: String = UUID.randomUUID().toString() @@ -158,8 +162,7 @@ class TelnyxClient( peerConnection?.getLocalDescription()?.description if (sessionDescriptionString == null) { callStateLiveData.postValue(CallState.ERROR) - } - else { + } else { val answerBodyMessage = SendingMessageBody( uuid, SocketMethod.ANSWER.methodName, CallParams( @@ -193,7 +196,7 @@ class TelnyxClient( destinationNumber: String, clientState: String, customHeaders: Map? = null - ) : Call { + ): Call { val inviteCall = call!!.copy( context = context, client = this, @@ -206,12 +209,11 @@ class TelnyxClient( val uuid: String = UUID.randomUUID().toString() val inviteCallId: UUID = UUID.randomUUID() - // set global call CallID callId = inviteCallId // Create new peer peerConnection = Peer( - context, client, providedTurn, providedStun, + context, client, providedTurn, providedStun, callId.toString(), object : PeerConnectionObserver() { override fun onIceCandidate(p0: IceCandidate?) { super.onIceCandidate(p0) @@ -392,11 +394,12 @@ class TelnyxClient( // Generate random UUID for sessid param, convert it to string and set globally sessid = UUID.randomUUID().toString() + socketResponseLiveData = + MutableLiveData>(SocketResponse.initialised()) socket = TxSocket( host_address = Config.TELNYX_PROD_HOST_ADDRESS, port = Config.TELNYX_PORT ) - registerNetworkCallback() } @@ -427,12 +430,22 @@ class TelnyxClient( * @param txPushMetaData, the push metadata used to connect to a call from push * (Get this from push notification - fcm data payload) * required fot push calls to work + * */ + @Deprecated("this telnyxclient.connect is deprecated." + + " Use telnyxclient.connect(providedServerConfig,txPushMetaData," + + "credential or tokenLogin) instead.") fun connect( providedServerConfig: TxServerConfiguration = TxServerConfiguration(), - txPushMetaData: String? + txPushMetaData: String? = null, ) { + socketResponseLiveData = + MutableLiveData>(SocketResponse.initialised()) + waitingForReg = true + invalidateGatewayResponseTimer() + resetGatewayCounters() + providedHostAddress = if (txPushMetaData != null) { val metadata = Gson().fromJson(txPushMetaData, PushMetaData::class.java) processCallFromPush(metadata) @@ -441,23 +454,122 @@ class TelnyxClient( providedServerConfig.host } + socket = TxSocket( + host_address = providedHostAddress!!, + port = providedServerConfig.port + ) + + providedPort = providedServerConfig.port + providedTurn = providedServerConfig.turn + providedStun = providedServerConfig.stun + if (ConnectivityHelper.isNetworkEnabled(context)) { + Timber.d("Provided Host Address: $providedHostAddress") + socket.connect(this, providedHostAddress, providedPort, pushMetaData) { + + } + } else { + socketResponseLiveData.postValue(SocketResponse.error("No Network Connection")) + } + } + + + /** + * Connects to the socket using this client as the listener + * Will respond with 'No Network Connection' if there is no network available + * @see [TxSocket] + * @param providedServerConfig, the TxServerConfiguration used to connect to the socket + * @param txPushMetaData, the push metadata used to connect to a call from push + * (Get this from push notification - fcm data payload) + * required fot push calls to work + * + * @param autoLogin, if true, the SDK will automatically log in with + * the provided credentials on connection established + * We recommend setting this to true + * + */ + fun connect( + providedServerConfig: TxServerConfiguration = TxServerConfiguration(), + credentialConfig: CredentialConfig, + txPushMetaData: String? = null, + autoLogin: Boolean = true, + ) { + + socketResponseLiveData = + MutableLiveData>(SocketResponse.initialised()) + waitingForReg = true invalidateGatewayResponseTimer() resetGatewayCounters() + providedHostAddress = if (txPushMetaData != null) { + val metadata = Gson().fromJson(txPushMetaData, PushMetaData::class.java) + processCallFromPush(metadata) + providedServerConfig.host + } else { + providedServerConfig.host + } + + socket = TxSocket( + host_address = providedHostAddress!!, + port = providedServerConfig.port + ) + + providedPort = providedServerConfig.port + providedTurn = providedServerConfig.turn + providedStun = providedServerConfig.stun + if (ConnectivityHelper.isNetworkEnabled(context)) { + Timber.d("Provided Host Address: $providedHostAddress") + socket.connect(this, providedHostAddress, providedPort, pushMetaData) { + if (autoLogin) { + credentialLogin(credentialConfig) + } + } + } else { + socketResponseLiveData.postValue(SocketResponse.error("No Network Connection")) + } + } + + fun connect( + providedServerConfig: TxServerConfiguration = TxServerConfiguration(), + tokenConfig: TokenConfig, + txPushMetaData: String? = null, + autoLogin: Boolean = true, + ) { + socketResponseLiveData = + MutableLiveData>(SocketResponse.initialised()) + waitingForReg = true + invalidateGatewayResponseTimer() + resetGatewayCounters() - Timber.d("Provided Host Address: $providedHostAddress") + providedHostAddress = if (txPushMetaData != null) { + val metadata = Gson().fromJson(txPushMetaData, PushMetaData::class.java) + processCallFromPush(metadata) + providedServerConfig.host + } else { + providedServerConfig.host + } + + socket = TxSocket( + host_address = providedHostAddress!!, + port = providedServerConfig.port + ) providedPort = providedServerConfig.port providedTurn = providedServerConfig.turn providedStun = providedServerConfig.stun if (ConnectivityHelper.isNetworkEnabled(context)) { - socket.connect(this, providedHostAddress, providedPort, pushMetaData) + Timber.d("Provided Host Address: $providedHostAddress") + socket.connect(this, providedHostAddress, providedPort, pushMetaData) { + if (autoLogin) { + tokenLogin(tokenConfig) + } + } } else { socketResponseLiveData.postValue(SocketResponse.error("No Network Connection")) } } + /** * Sets the callOngoing state to true. This can be used to see if the SDK thinks a call is ongoing. */ @@ -528,7 +640,9 @@ class TelnyxClient( * @param config, the CredentialConfig used to log in * @see [CredentialConfig] */ + @Deprecated("telnyxclient.credentialLogin is deprecated. Use telnyxclient.connect(..) instead.") fun credentialLogin(config: CredentialConfig) { + val uuid: String = UUID.randomUUID().toString() val user = config.sipUser val password = config.sipPassword @@ -571,6 +685,7 @@ class TelnyxClient( sessid = sessid ) ) + Timber.d("Auto login with credentialConfig") socket.send(loginMessage) } @@ -648,6 +763,8 @@ class TelnyxClient( * @param config, the TokenConfig used to log in * @see [TokenConfig] */ + @Deprecated("telnyxclient.tokenLogin is deprecated. Use telnyxclient.connect(...,autoLogin:true) " + + "with autoLogin set to true instead.") fun tokenLogin(config: TokenConfig) { val uuid: String = UUID.randomUUID().toString() val token = config.sipToken @@ -683,6 +800,39 @@ class TelnyxClient( socket.send(loginMessage) } + internal fun startStats(sessionId: UUID) { + debugReportStarted = true + val loginMessage = InitiateOrStopStatPrams( + type = "debug_report_start", + debugReportId = sessionId.toString(), + ) + socket.send(loginMessage) + } + + /** + * Sends Logged webrtc stats to backend + * + * @param config, the TokenConfig used to log in + * @see [TokenConfig] + */ + internal fun sendStats(data: JsonObject, sessionId: UUID) { + + val loginMessage = StatPrams( + debugReportId = sessionId.toString(), + reportData = data + ) + socket.send(loginMessage) + + } + + internal fun stopStats(sessionId: UUID) { + debugReportStarted = false + val loginMessage = InitiateOrStopStatPrams( + debugReportId = sessionId.toString(), + ) + socket.send(loginMessage) + } + /** * Sets the global SDK log level * Logging is implemented with Timber @@ -764,7 +914,7 @@ class TelnyxClient( } else { SpeakerMode.EARPIECE } - }else{ + } else { SpeakerMode.EARPIECE } @@ -804,10 +954,12 @@ class TelnyxClient( SpeakerMode.SPEAKER -> { audioManager?.isSpeakerphoneOn = true } + SpeakerMode.EARPIECE -> { audioManager?.isSpeakerphoneOn = false } - SpeakerMode.UNASSIGNED -> audioManager?.isSpeakerphoneOn = false + + SpeakerMode.UNASSIGNED -> audioManager?.isSpeakerphoneOn = false } } @@ -843,7 +995,7 @@ class TelnyxClient( * Stops any audio that the MediaPlayer is playing * @see [MediaPlayer] */ - internal fun stopMediaPlayer() { + private fun stopMediaPlayer() { if (mediaPlayer != null) { mediaPlayer!!.stop() mediaPlayer!!.reset() @@ -1035,6 +1187,7 @@ class TelnyxClient( override fun onConnectionEstablished() { Timber.d("[%s] :: onConnectionEstablished", this@TelnyxClient.javaClass.simpleName) socketResponseLiveData.postValue(SocketResponse.established()) + } override fun onErrorReceived(jsonObject: JsonObject) { @@ -1044,6 +1197,7 @@ class TelnyxClient( } override fun onByeReceived(callId: UUID) { + Timber.d("[%s] :: onByeReceived", this.javaClass.simpleName) val byeCall = calls[callId] byeCall?.apply { @@ -1150,20 +1304,46 @@ class TelnyxClient( // generally occurs when a ringback setting is applied in inbound call settings earlySDP = true + val callerIDName = + if (params.has("caller_id_name")) params.get("caller_id_name").asString else "" + val callerNumber = + if (params.has("caller_id_number")) params.get("caller_id_number").asString else "" + + val mediaResponse = MediaResponse( + UUID.fromString(callId), + callerIDName, + callerNumber, + sessionId, + ) + client.socketResponseLiveData.postValue( + SocketResponse.messageReceived( + ReceivedMessageBody( + SocketMethod.MEDIA.methodName, + mediaResponse + ) + ) + ) + } else { // There was no SDP in the response, there was an error. callStateLiveData.postValue(CallState.DONE) client.removeFromCalls(UUID.fromString(callId)) } + } + /*Stop local Media and play ringback from telnyx cloud*/ stopMediaPlayer() } override fun onOfferReceived(jsonObject: JsonObject) { if (jsonObject.has("params")) { - Timber.d("[%s] :: onOfferReceived [%s]", this@TelnyxClient.javaClass.simpleName, jsonObject) + Timber.d( + "[%s] :: onOfferReceived [%s]", + this@TelnyxClient.javaClass.simpleName, + jsonObject + ) val offerCall = call!!.copy( context = context, client = this, @@ -1173,6 +1353,7 @@ class TelnyxClient( providedTurn = providedTurn!!, providedStun = providedStun!! ).apply { + val params = jsonObject.getAsJsonObject("params") val offerCallId = UUID.fromString(params.get("callID").asString) val remoteSdp = params.get("sdp").asString @@ -1188,7 +1369,7 @@ class TelnyxClient( val customHeaders = params.get("dialogParams")?.asJsonObject?.get("custom_headers")?.asJsonArray peerConnection = Peer( - context, client, providedTurn, providedStun, + context, client, providedTurn, providedStun, offerCallId.toString(), object : PeerConnectionObserver() { override fun onIceCandidate(p0: IceCandidate?) { super.onIceCandidate(p0) @@ -1219,6 +1400,7 @@ class TelnyxClient( this.inviteResponse = inviteResponse } + offerCall.client.playRingtone() addToCalls(offerCall) offerCall.client.socketResponseLiveData.postValue( SocketResponse.messageReceived( @@ -1228,7 +1410,6 @@ class TelnyxClient( ) ) ) - offerCall.client.playRingtone() } else { Timber.d( "[%s] :: Invalid offer received, missing required parameters [%s]", @@ -1316,7 +1497,7 @@ class TelnyxClient( val callerNumber = params.get("caller_id_number").asString peerConnection = Peer( - context, client, providedTurn, providedStun, + context, client, providedTurn, providedStun, callId.toString(), object : PeerConnectionObserver() { override fun onIceCandidate(p0: IceCandidate?) { super.onIceCandidate(p0) diff --git a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxConfig.kt b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxConfig.kt index 35640e60..c36094a6 100644 --- a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxConfig.kt +++ b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxConfig.kt @@ -41,7 +41,7 @@ data class CredentialConfig( val ringtone: Any?, val ringBackTone: Int?, val logLevel: LogLevel = LogLevel.NONE, - val autoReconnect: Boolean = true + val autoReconnect: Boolean = false ) : TelnyxConfig() /** diff --git a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/model/PushMetaData.kt b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/model/PushMetaData.kt index ea65fe05..da82183c 100644 --- a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/model/PushMetaData.kt +++ b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/model/PushMetaData.kt @@ -1,5 +1,6 @@ package com.telnyx.webrtc.sdk.model +import com.google.gson.Gson import com.google.gson.annotations.SerializedName data class PushMetaData( @@ -15,6 +16,9 @@ data class PushMetaData( val rtcIP: String? = null, @SerializedName("rtc_port") val rtcPort: Int? = null, + ) { + fun toJson() : String { + return Gson() .toJson(this) + } - - ) +} diff --git a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/peer/Peer.kt b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/peer/Peer.kt index 0a909c19..b4690bda 100644 --- a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/peer/Peer.kt +++ b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/peer/Peer.kt @@ -5,16 +5,31 @@ package com.telnyx.webrtc.sdk.peer import android.content.Context +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonObject import com.telnyx.webrtc.sdk.Config.DEFAULT_STUN import com.telnyx.webrtc.sdk.Config.DEFAULT_TURN import com.telnyx.webrtc.sdk.Config.PASSWORD import com.telnyx.webrtc.sdk.Config.USERNAME import com.telnyx.webrtc.sdk.TelnyxClient import com.telnyx.webrtc.sdk.socket.TxSocket -import org.webrtc.* +import org.webrtc.AudioSource +import org.webrtc.AudioTrack +import org.webrtc.DefaultVideoDecoderFactory +import org.webrtc.DefaultVideoEncoderFactory +import org.webrtc.EglBase +import org.webrtc.IceCandidate +import org.webrtc.MediaConstraints +import org.webrtc.PeerConnection +import org.webrtc.PeerConnectionFactory +import org.webrtc.SdpObserver +import org.webrtc.SessionDescription import timber.log.Timber import java.util.* + /** * Peer class that represents a peer connection which is required to initiate a call. * @@ -27,16 +42,22 @@ internal class Peer( val client: TelnyxClient, private val providedTurn: String = DEFAULT_TURN, private val providedStun: String = DEFAULT_STUN, + private val callId: String = "", observer: PeerConnection.Observer ) { companion object { private const val AUDIO_LOCAL_TRACK_ID = "audio_local_track" private const val AUDIO_LOCAL_STREAM_ID = "audio_local_stream" + private const val CANDIDATE_LIMIT : Int = 5 + private const val STATS_INTERVAL : Long = 2000L + private const val STATS_INITIAL : Long = 0L } private val rootEglBase: EglBase = EglBase.create() + internal var debugStatsId = UUID.randomUUID() + private val iceServer = getIceServers() @@ -133,13 +154,75 @@ internal class Peer( localAudioTrack.setVolume(1.0) localStream.addTrack(localAudioTrack) peerConnection?.addTrack(localAudioTrack) - peerConnection?.getStats { - it.statsMap.forEach { (key, value) -> - Timber.tag("Stats").d("Key: $key, Value: $value") - } + } + + var gson: Gson = GsonBuilder().setPrettyPrinting().create() + private val timer = Timer() + var mainObject: JsonObject = JsonObject() + var audio: JsonObject = JsonObject() + var statsData: JsonObject = JsonObject() + var inBoundStats: JsonArray = JsonArray() + var outBoundStats: JsonArray = JsonArray() + var candidateParis: JsonArray = JsonArray() + + internal fun stopTimer() { + client.stopStats(debugStatsId) + debugStatsId = null + mainObject = JsonObject() + timer.cancel() + } + + internal fun startTimer() { + if (!client.debugReportStarted){ + debugStatsId = UUID.randomUUID() + client.startStats(debugStatsId) } + timer.schedule(object : TimerTask() { + override fun run() { + mainObject.addProperty("event", "stats") + mainObject.addProperty("tag", "stats") + mainObject.addProperty("peerId", "stats") + mainObject.addProperty("connectionId", callId) + peerConnection?.getStats { + it.statsMap.forEach { (key, value) -> + if (value.type == "inbound-rtp") { + val jsonInbound = gson.toJsonTree(value) + inBoundStats.add(jsonInbound) + } + if (value.type == "outbound-rtp") { + val jsonOutbound = gson.toJsonTree(value) + outBoundStats.add(jsonOutbound) + } + if (value.type == "candidate-pair" && candidateParis.size() < CANDIDATE_LIMIT) { + val jsonCandidatePair = gson.toJsonTree(value) + candidateParis.add(jsonCandidatePair) + } + + } + } + audio.add("inbound", inBoundStats) + audio.add("outbound", outBoundStats) + audio.add("candidatePair", candidateParis) + statsData.add("audio", audio) + mainObject.add("data", statsData) + mainObject.addProperty("timestamp", System.currentTimeMillis()) + if (inBoundStats.size() > 0 && outBoundStats.size() > 0 && candidateParis.size() > 0) { + inBoundStats = JsonArray() + outBoundStats = JsonArray() + candidateParis = JsonArray() + statsData = JsonObject() + audio = JsonObject() + Timber.tag("Stats Inbound").d("Inbound: ${mainObject.toString()}") + if (debugStatsId != null){ + client.sendStats(mainObject, debugStatsId) + } + } + + } + }, STATS_INITIAL, STATS_INTERVAL) } + /** * Initiates a call, creating an offer with a local SDP * The offer creation is handled with an [SdpObserver] @@ -149,6 +232,7 @@ internal class Peer( private fun PeerConnection.call(sdpObserver: SdpObserver) { val constraints = MediaConstraints().apply { mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) + mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")) optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")) } @@ -191,6 +275,7 @@ internal class Peer( private fun PeerConnection.answer(sdpObserver: SdpObserver) { val constraints = MediaConstraints().apply { mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) + mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")) optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")) } @@ -299,6 +384,7 @@ internal class Peer( disconnect() peerConnectionFactory.dispose() } + stopTimer() } init { diff --git a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/socket/TxSocket.kt b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/socket/TxSocket.kt index 4c8ba61b..353a8c82 100644 --- a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/socket/TxSocket.kt +++ b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/socket/TxSocket.kt @@ -128,9 +128,9 @@ class TxSocket( "[%s] Connection established :: $host_address", this@TxSocket.javaClass.simpleName ) + isConnected = true onConnected(true) listener.onConnectionEstablished() - isConnected = true } override fun onMessage(webSocket: WebSocket, text: String) { @@ -318,13 +318,6 @@ class TxSocket( socket.cancel() // socket.close(1000, "Websocket connection was asked to close") } - if (this::client.isInitialized) { - launch(Dispatchers.IO) { - client.dispatcher.executorService.shutdown() - client.connectionPool.evictAll() - client.cache?.close() - } - } job.cancel("Socket was destroyed, cancelling attached job") } diff --git a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/receive/ReceivedResult.kt b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/receive/ReceivedResult.kt index f6d4db79..bbfb85c6 100644 --- a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/receive/ReceivedResult.kt +++ b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/receive/ReceivedResult.kt @@ -4,16 +4,9 @@ package com.telnyx.webrtc.sdk.verto.receive -import android.os.Parcel -import android.os.Parcelable import com.google.gson.annotations.SerializedName import com.telnyx.webrtc.sdk.CustomHeaders -import com.telnyx.webrtc.sdk.utilities.parseObject -import com.telnyx.webrtc.sdk.utilities.toJsonString -import kotlinx.parcelize.Parceler -import kotlinx.parcelize.Parcelize import java.util.* -import kotlin.collections.ArrayList /** * Class representations of responses received on the socket connection @@ -21,13 +14,13 @@ import kotlin.collections.ArrayList sealed class ReceivedResult -@Parcelize + data class DisablePushResponse( @SerializedName("message") val success: Boolean, @SerializedName("message") val message: String -) : ReceivedResult(), Parcelable { +) : ReceivedResult() { companion object { // Refactor for backend to send a boolean instead of a string @@ -40,11 +33,10 @@ data class DisablePushResponse( * * @param sessid the session ID provided after logging in. */ -@Parcelize data class LoginResponse( @SerializedName("sessid") val sessid: String -) : ReceivedResult(), Parcelable +) : ReceivedResult() data class ByeResponse( @@ -58,7 +50,6 @@ data class ByeResponse( * @param callId a unique UUID that represents each ongoing call. * @param sdp the Session Description Protocol that is received as a part of the answer to the call. */ -@Parcelize data class AnswerResponse( @SerializedName("callID") val callId: UUID, @@ -66,24 +57,7 @@ data class AnswerResponse( val sdp: String, @SerializedName("custom_headers") val customHeaders: ArrayList = arrayListOf() -) : ReceivedResult(), Parcelable { - private companion object : Parceler { - override fun AnswerResponse.write(parcel: Parcel, flags: Int) { - parcel.writeString(sdp) - parcel.writeString(callId.toString()) - parcel.writeString(customHeaders.toJsonString()) - } - - override fun create(parcel: Parcel): AnswerResponse { - return AnswerResponse( - callId = UUID.fromString(parcel.readString()), - sdp = parcel.readString()!!, - customHeaders = parcel.readString()?.parseObject>() ?: arrayListOf() - ) - } - } - -} +) : ReceivedResult() /** * An invitation response containing the required information @@ -94,7 +68,6 @@ data class AnswerResponse( * @param callerIdNumber the number of the person who sent the invitation * @param sessid the Telnyx Session ID on the socket connection. */ -@Parcelize data class InviteResponse( @SerializedName("callID") val callId: UUID, @@ -108,29 +81,7 @@ data class InviteResponse( val sessid: String, @SerializedName("custom_headers") val customHeaders: ArrayList = arrayListOf() -) : ReceivedResult(), Parcelable { - private companion object : Parceler { - override fun InviteResponse.write(parcel: Parcel, flags: Int) { - parcel.writeString(sdp) - parcel.writeString(callId.toString()) - parcel.writeString(callerIdNumber) - parcel.writeString(callerIdName) - parcel.writeString(sessid) - parcel.writeString(customHeaders.toJsonString()) - } - - override fun create(parcel: Parcel): InviteResponse { - return InviteResponse( - callId = UUID.fromString(parcel.readString()), - sdp = parcel.readString()!!, - callerIdNumber = parcel.readString()!!, - callerIdName = parcel.readString()!!, - sessid = parcel.readString()!!, - customHeaders = parcel.readString()?.parseObject>() ?: arrayListOf() - ) - } - } -} +) : ReceivedResult() data class RingingResponse( @SerializedName("callID") @@ -144,3 +95,15 @@ data class RingingResponse( @SerializedName("custom_headers") val customHeaders: ArrayList = arrayListOf() ) : ReceivedResult() + + +data class MediaResponse( + @SerializedName("callID") + val callId: UUID, + @SerializedName("callerIdName") + val callerIdName: String, + @SerializedName("callerIdNumber") + val callerIdNumber: String, + @SerializedName("sessid") + val sessid: String +) : ReceivedResult() diff --git a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/receive/SocketResponse.kt b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/receive/SocketResponse.kt index a90d38b7..298b3969 100644 --- a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/receive/SocketResponse.kt +++ b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/receive/SocketResponse.kt @@ -6,7 +6,7 @@ package com.telnyx.webrtc.sdk.verto.receive import com.telnyx.webrtc.sdk.model.SocketStatus -data class SocketResponse(val status: SocketStatus, val data: T?, val errorMessage: String?) { +data class SocketResponse(var status: SocketStatus, val data: T?, val errorMessage: String?) { companion object { fun established(): SocketResponse { return SocketResponse( @@ -16,6 +16,14 @@ data class SocketResponse(val status: SocketStatus, val data: T?, val err ) } + fun initialised(): SocketResponse { + return SocketResponse( + SocketStatus.ESTABLISHED, + null, + null + ) + } + fun messageReceived(data: T): SocketResponse { return SocketResponse( SocketStatus.MESSAGERECEIVED, diff --git a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/send/ParamRequest.kt b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/send/ParamRequest.kt index 8cc17d79..cf0982c9 100644 --- a/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/send/ParamRequest.kt +++ b/telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/verto/send/ParamRequest.kt @@ -7,6 +7,8 @@ package com.telnyx.webrtc.sdk.verto.send import com.google.gson.JsonObject import com.google.gson.annotations.SerializedName import com.telnyx.webrtc.sdk.telnyx_rtc.BuildConfig +import java.util.UUID + sealed class ParamRequest data class LoginParam( @@ -21,6 +23,30 @@ data class LoginParam( val userAgent: String = "Android-" + BuildConfig.SDK_VERSION.toString(), ) : ParamRequest() +data class StatPrams( + val type: String = "debug_report_data", + @SerializedName("debug_report_id") + val debugReportId: String = UUID.randomUUID().toString(), + @SerializedName("debug_report_data") + val reportData: JsonObject, + @SerializedName("debug_report_version") + val debugReportVersion: Int = 1, + @SerializedName("id") + val id: String = UUID.randomUUID().toString(), + val jsonrpc:String = "2.0" +) : ParamRequest() + +data class InitiateOrStopStatPrams( + val type: String = "debug_report_stop", + @SerializedName("debug_report_id") + val debugReportId: String = UUID.randomUUID().toString(), + @SerializedName("debug_report_version") + val debugReportVersion: Int = 1, + @SerializedName("id") + val id: String = UUID.randomUUID().toString(), + val jsonrpc:String = "2.0" +) : ParamRequest() + data class CallParams( diff --git a/telnyx_rtc/src/test/java/com/telnyx/webrtc/sdk/TelnyxClientTest.kt b/telnyx_rtc/src/test/java/com/telnyx/webrtc/sdk/TelnyxClientTest.kt index 5ea2771c..f00aba7e 100644 --- a/telnyx_rtc/src/test/java/com/telnyx/webrtc/sdk/TelnyxClientTest.kt +++ b/telnyx_rtc/src/test/java/com/telnyx/webrtc/sdk/TelnyxClientTest.kt @@ -141,6 +141,13 @@ class TelnyxClientTest : BaseTest() { assertEquals(client.isNetworkCallbackRegistered, true) } + @Test + fun `checkForMockCredentials`() { + assertEquals(MOCK_USERNAME_TEST, "") + assertEquals(MOCK_PASSWORD, "") + } + + @Test fun `disconnect connection`() { client.socket = Mockito.spy( @@ -176,16 +183,16 @@ class TelnyxClientTest : BaseTest() { @Test fun `login with valid credentials - login sent to socket and json received`() { client = Mockito.spy(TelnyxClient(mockContext)) + client.connect(txPushMetaData = null) client.socket = Mockito.spy( TxSocket( host_address = "rtc.telnyx.com", port = 14938, ) ) - client.connect(txPushMetaData = null) val config = CredentialConfig( - MOCK_USERNAME, + MOCK_USERNAME_TEST, MOCK_PASSWORD, "Test", "000000000", @@ -203,13 +210,13 @@ class TelnyxClientTest : BaseTest() { @Test fun `login with invalid credentials - login sent to socket and json received`() { client = Mockito.spy(TelnyxClient(mockContext)) + client.connect(txPushMetaData = null) client.socket = Mockito.spy( TxSocket( host_address = "rtc.telnyx.com", port = 14938, ) ) - client.connect(txPushMetaData = null) val config = CredentialConfig( "asdfasass", @@ -233,6 +240,7 @@ class TelnyxClientTest : BaseTest() { @Test fun `login with valid token - login sent to socket and json received`() { client = Mockito.spy(TelnyxClient(mockContext)) + client.connect(txPushMetaData = null) client.socket = Mockito.spy( TxSocket( host_address = "rtc.telnyx.com", @@ -240,7 +248,6 @@ class TelnyxClientTest : BaseTest() { ) ) - client.connect(txPushMetaData = null) val config = TokenConfig( MOCK_TOKEN, @@ -261,6 +268,9 @@ class TelnyxClientTest : BaseTest() { @Test fun `login with invalid token - login sent to socket and json received`() { client = Mockito.spy(TelnyxClient(mockContext)) + + + client.connect(txPushMetaData = null) client.socket = Mockito.spy( TxSocket( host_address = "rtc.telnyx.com", @@ -268,8 +278,6 @@ class TelnyxClientTest : BaseTest() { ) ) - client.connect(txPushMetaData = null) - val config = TokenConfig( anyString(), "test", diff --git a/telnyx_rtc/src/test/java/com/telnyx/webrtc/sdk/testhelpers/TestConstants.kt b/telnyx_rtc/src/test/java/com/telnyx/webrtc/sdk/testhelpers/TestConstants.kt index 03d5cc99..0603874c 100644 --- a/telnyx_rtc/src/test/java/com/telnyx/webrtc/sdk/testhelpers/TestConstants.kt +++ b/telnyx_rtc/src/test/java/com/telnyx/webrtc/sdk/testhelpers/TestConstants.kt @@ -1,5 +1,5 @@ package com.telnyx.webrtc.sdk.testhelpers -const val MOCK_USERNAME = "" +const val MOCK_USERNAME_TEST = "" const val MOCK_PASSWORD = "" const val MOCK_TOKEN = ""