Skip to content

Commit

Permalink
added abilities to make/take calls
Browse files Browse the repository at this point in the history
  • Loading branch information
konaire committed Feb 24, 2018
1 parent 9c23682 commit 3392e93
Show file tree
Hide file tree
Showing 13 changed files with 460 additions and 48 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Expand Up @@ -27,6 +27,6 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"

implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "org.greenrobot:eventbus:$eventbusVersion"
}
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Expand Up @@ -48,5 +48,11 @@
android:screenOrientation="portrait" />

<service android:exported="false" android:name=".call.CallService" />

<receiver android:exported="false" android:name=".call.CallReceiver">
<intent-filter>
<action android:name="io.codebeavers.sipcaller.INCOMING_CALL" />
</intent-filter>
</receiver>
</application>
</manifest>
24 changes: 24 additions & 0 deletions app/src/main/java/io/codebeavers/sipcaller/call/CallReceiver.kt
@@ -0,0 +1,24 @@
package io.codebeavers.sipcaller.call

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

import io.codebeavers.sipcaller.events.CallEvent

import org.greenrobot.eventbus.EventBus

/**
* Created by Evgeny Eliseyev on 23/02/2018.
* Sends event about an incoming call to the running activity.
*/

class CallReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null) {
return
}

EventBus.getDefault().post(CallEvent(intent))
}
}
213 changes: 189 additions & 24 deletions app/src/main/java/io/codebeavers/sipcaller/call/CallService.kt
Expand Up @@ -3,10 +3,13 @@ package io.codebeavers.sipcaller.call
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.IntentFilter
import android.media.ToneGenerator
import android.net.sip.*
import android.os.*

import io.codebeavers.sipcaller.util.Const
import io.codebeavers.sipcaller.util.SoundManager

/**
* Created by Evgeny Eliseyev on 21/02/2018.
Expand All @@ -19,33 +22,140 @@ class CallService: Service(), SipRegistrationListener {

private var mManager: SipManager? = null
private var mProfile: SipProfile? = null
private var dataIntent: Intent? = null
private var mCall: SipAudioCall? = null
private var mDataIntent: Intent? = null
private var mSound: SoundManager? = null
private var mReceiver: DataReceiver? = null

override fun onBind(intent: Intent?): IBinder {
val filter = IntentFilter(Const.ACTION_DATA_TO_SERVICE_EXCHANGE)

initializeManager()
unregisterReceiver()
mReceiver = DataReceiver()
registerReceiver(mReceiver, filter)
mSound = SoundManager.getInstance(this)

return CallBinder()
}

override fun onUnbind(intent: Intent?): Boolean {
endCall()
closeLocalProfile()
unregisterReceiver()
mSound = null

return super.onUnbind(intent)
}

override fun onRegistering(localProfileUri: String?) {
sendBroadcast(getIntentForStatus(Const.SipRegistration.STARTED))
sendBroadcast(getIntentForStatusOfRegistration(Const.SipRegistration.STARTED))
}

override fun onRegistrationDone(localProfileUri: String?, expiryTime: Long) {
dataIntent = getIntentForStatus(Const.SipRegistration.FINISHED)
mDataIntent = getIntentForStatusOfRegistration(Const.SipRegistration.FINISHED)
}

// Send error only if there was no other error or successful registration.
override fun onRegistrationFailed(localProfileUri: String?, errorCode: Int, errorMessage: String?) {
dataIntent = getIntentForStatus(Const.SipRegistration.ERROR, errorCode)
val hasStatus = mDataIntent?.extras?.containsKey(Const.KEY_STATUS) ?: false
if (hasStatus) {
return
}

mDataIntent = getIntentForStatusOfRegistration(Const.SipRegistration.ERROR, errorCode)
}

fun makeAudioCall(sipAddress: String): Boolean {
val listener = object : SipAudioCall.Listener() {
private val handler = Handler()

// Stops dial tones and starts audio stream.
override fun onCallEstablished(call: SipAudioCall) {
call.startAudio()
call.setSpeakerMode(false)

if (call.isMuted) {
call.toggleMute()
}

mSound?.stopTone()
}

// Generates busy dial tone and sends call end status to activity.
override fun onCallBusy(call: SipAudioCall) {
mSound?.startTone(ToneGenerator.TONE_SUP_BUSY)
handler.postDelayed({ mSound?.stopTone() }, 3000)

sendBroadcast(getIntentForStatusOfCall(Const.SipCall.ENDED))
}

// Generates one beep and sends call end status to activity.
override fun onCallEnded(call: SipAudioCall) {
mSound?.startTone(ToneGenerator.TONE_PROP_PROMPT)
handler.postDelayed({ mSound?.stopTone() }, 100)

sendBroadcast(getIntentForStatusOfCall(Const.SipCall.ENDED))
}
}

mCall = try {
mManager?.makeAudioCall(mProfile?.uriString, sipAddress, listener, Const.CALL_TIMEOUT)
} catch (e: Exception) {
if (mProfile != null) {
closeLocalProfile()
}

null
}

return mCall != null
}

fun takeAudioCall(intent: Intent): String? {
val listener = object: SipAudioCall.Listener() {
private val handler = Handler()

// Stops ringing when call is answered.
override fun onCallEstablished(call: SipAudioCall) {
mSound?.stopRinging()
}

// Generates one beep or stops ringing.
// It depends on whether you answered the call or not.
// Also the method sends call end status to activity.
override fun onCallEnded(call: SipAudioCall) {
if (call.isInCall) {
mSound?.startTone(ToneGenerator.TONE_PROP_PROMPT)
handler.postDelayed({ mSound?.stopTone() }, 100)
} else {
mSound?.stopRinging()
}

sendBroadcast(getIntentForStatusOfCall(Const.SipCall.ENDED))
}
}

mCall = try {
mManager?.takeAudioCall(intent, listener)
} catch (e: Exception) {
null
}

return mCall?.peerProfile?.userName
}

fun doAction(status: Const.SipCall) {
if (status == Const.SipCall.ANSWERED) {
answerCall()
} else if (status == Const.SipCall.ENDED) {
endCall()
}
}

private fun initializeManager() {
if (!SipManager.isApiSupported(this)) {
sendBroadcast(getIntentForStatus(Const.SipRegistration.ERROR, SipErrorCode.CLIENT_ERROR))
sendBroadcast(getIntentForStatusOfRegistration(Const.SipRegistration.ERROR, SipErrorCode.CLIENT_ERROR))
} else if (mManager == null) {
mManager = SipManager.newInstance(this)
}
Expand All @@ -54,28 +164,32 @@ class CallService: Service(), SipRegistrationListener {
}

private fun initializeLocalProfile() {
val intent = Intent()
val domain = Const.SIP_URL
val login = Const.SIP_LOGIN
val password = Const.SIP_PASSWORD
val builder = SipProfile.Builder(login, domain)
intent.action = Const.ACTION_INCOMING_CALL
builder.setPassword(password)
mProfile = builder.build()

// Android will call special BroadcastReceiver when an incoming call will be arrived.
// We should set SipRegistrationListener after opening profile, otherwise it willn't be called because of bug.
mManager?.open(mProfile, PendingIntent.getBroadcast(this, 0, intent, Intent.FILL_IN_DATA), null)
mManager?.setRegistrationListener(mProfile!!.uriString, this)
try {
val intent = Intent()
val domain = Const.SIP_URL
val login = Const.SIP_LOGIN
val password = Const.SIP_PASSWORD
val builder = SipProfile.Builder(login, domain)
intent.action = Const.ACTION_INCOMING_CALL
builder.setPassword(password)
mProfile = builder.build()

// Android will call special BroadcastReceiver when an incoming call will be arrived.
// We should set SipRegistrationListener after opening profile, otherwise it willn't be called because of bug.
mManager?.open(mProfile, PendingIntent.getBroadcast(this, 0, intent, Intent.FILL_IN_DATA), null)
mManager?.setRegistrationListener(mProfile!!.uriString, this)
} catch (e: Exception) {
e.printStackTrace()
}

// Sometimes registration hangs so we need to send data after a small timeout.
Handler(Looper.getMainLooper()).postDelayed({
if (dataIntent == null) {
dataIntent = getIntentForStatus(Const.SipRegistration.ERROR, SipErrorCode.TIME_OUT)
if (mDataIntent == null) {
mDataIntent = getIntentForStatusOfRegistration(Const.SipRegistration.ERROR, SipErrorCode.TIME_OUT)
}

sendBroadcast(dataIntent)
}, 2000)
sendBroadcast(mDataIntent)
}, 5000)
}

private fun closeLocalProfile() {
Expand All @@ -85,19 +199,70 @@ class CallService: Service(), SipRegistrationListener {

if (manager != null && profile != null) {
manager.unregister(profile, this)
manager.close(profile.uriString)
}
} catch (e: Exception) {
e.printStackTrace()
}
}

private fun answerCall() {
val call = mCall ?: return

try {
call.answerCall(Const.CALL_TIMEOUT)
call.setSpeakerMode(false)
call.startAudio()

if (call.isMuted) {
call.toggleMute()
}
} catch (e: Exception) {
call.close()
}
}

private fun endCall() {
val call = mCall ?: return

try {
// Stops dial tones for outgoing call.
// Stops ringing for incoming call.
if (!call.isInCall) {
mSound?.stopTone()
mSound?.stopRinging()
}

call.endCall()
} catch (e: SipException) {
e.printStackTrace()
}

call.close()
}

// Generates intent to send registration status to activity via receiver.
private fun getIntentForStatus(status: Const.SipRegistration, errorCode: Int = 0): Intent {
private fun getIntentForStatusOfRegistration(status: Const.SipRegistration, errorCode: Int = 0): Intent {
val intent = Intent()

intent.action = Const.ACTION_DATA_EXCHANGE
intent.action = Const.ACTION_DATA_TO_ACTIVITY_EXCHANGE
intent.putExtra(Const.KEY_ERROR_CODE, errorCode)
intent.putExtra(Const.KEY_STATUS, status)
return intent
}

private fun getIntentForStatusOfCall(status: Const.SipCall): Intent {
val intent = Intent()

intent.action = Const.ACTION_DATA_TO_ACTIVITY_EXCHANGE
intent.putExtra(Const.KEY_STATUS, status)
return intent
}

private fun unregisterReceiver() {
if (mReceiver != null) {
unregisterReceiver(mReceiver)
mReceiver = null
}
}
}
17 changes: 15 additions & 2 deletions app/src/main/java/io/codebeavers/sipcaller/call/DataReceiver.kt
Expand Up @@ -4,11 +4,14 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

import io.codebeavers.sipcaller.ui.CallActivity
import io.codebeavers.sipcaller.ui.MainActivity
import io.codebeavers.sipcaller.util.Const

/**
* Created by Evgeny Eliseyev on 21/02/2018.
* Receives statuses about registration and send them to activity.
* Sends statuses about call between service and activity for both sides.
*/

class DataReceiver: BroadcastReceiver() {
Expand All @@ -17,13 +20,23 @@ class DataReceiver: BroadcastReceiver() {
return
}

val hasData = intent.extras?.containsKey(Const.KEY_STATUS) ?: false
val hasErrorCode = intent.extras?.containsKey(Const.KEY_ERROR_CODE) ?: false
val hasStatus = intent.extras?.containsKey(Const.KEY_STATUS) ?: false
val isRegistration = hasStatus && hasErrorCode
val isCall = hasStatus && !hasErrorCode

if (context is MainActivity && hasData && hasErrorCode) {
if (context is MainActivity && isRegistration) {
val status = intent.getSerializableExtra(Const.KEY_STATUS) as Const.SipRegistration
val errorCode = intent.getIntExtra(Const.KEY_ERROR_CODE, 0)
context.receiveRegisterState(status, errorCode)
} else if (isCall) {
val status = intent.getSerializableExtra(Const.KEY_STATUS) as Const.SipCall

if (context is CallActivity) {
context.updateCallStatus(status)
} else if (context is CallService) {
context.doAction(status)
}
}
}
}
@@ -0,0 +1,9 @@
package io.codebeavers.sipcaller.events

import android.content.Intent

/**
* Created by Evgeny Eliseyev on 23/02/2018.
*/

data class CallEvent(val intent: Intent)

0 comments on commit 3392e93

Please sign in to comment.