Files
Juha Heinanen db7538f9db - fixed bug in checking of record audio permission
- do not load baresip lib if it has already been loaded
- removed call uri adapter when call comes in
2018-11-23 16:52:35 +02:00

666 lines
28 KiB
Kotlin

package com.tutpro.baresip
import android.annotation.TargetApi
import android.app.*
import android.app.Notification.VISIBILITY_PUBLIC
import android.content.*
import android.media.*
import android.net.ConnectivityManager
import android.net.NetworkInfo
import android.net.wifi.WifiManager
import android.os.IBinder
import android.os.PowerManager
import android.support.annotation.Keep
import android.support.v4.app.NotificationCompat
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import java.io.File
import android.support.v4.content.LocalBroadcastManager
import android.os.Build
import android.support.v4.app.NotificationCompat.VISIBILITY_PRIVATE
import java.nio.charset.StandardCharsets
import java.io.InputStream
import java.util.*
class BaresipService: Service() {
private val LOG_TAG = "Baresip Service"
internal lateinit var intent: Intent
internal lateinit var am: AudioManager
internal lateinit var rt: Ringtone
internal lateinit var nm: NotificationManager
internal lateinit var snb: NotificationCompat.Builder
internal lateinit var npi: PendingIntent
internal lateinit var nr: BroadcastReceiver
internal lateinit var wl: PowerManager.WakeLock
internal lateinit var fl: WifiManager.WifiLock
internal var rtTimer: Timer? = null
internal var filesPath = ""
internal var restarting = false
override fun onCreate() {
Log.d(LOG_TAG, "At onCreate")
intent = Intent("com.tutpro.baresip.EVENT")
intent.setPackage("com.tutpro.baresip")
filesPath = applicationContext.filesDir.absolutePath
am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val rtUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext,
RingtoneManager.TYPE_RINGTONE)
rt = RingtoneManager.getRingtone(applicationContext, rtUri)
nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
createNotificationChannels()
snb = NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID)
val ni = Intent(this, MainActivity::class.java)
.setAction(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
npi = PendingIntent.getActivity(this, 0, ni, 0)
nr = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
val isConnected: Boolean = activeNetwork?.isConnectedOrConnecting == true
if (isConnected) {
Log.d(LOG_TAG, "Network is connected/connecting")
if (disconnected) {
UserAgent.register(uas)
disconnected = false
}
} else {
Log.d(LOG_TAG, "Network is NOT connected/connecting")
disconnected = true
}
}
}
}
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "com.tutpro.baresip:wakelog")
/* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:$packageName")
startActivity(intent)
} */
super.onCreate()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val action: String
if (intent == null) {
action = "Start"
Log.d(LOG_TAG, "Received onStartCommand with null intent")
} else {
action = intent.getAction()
Log.d(LOG_TAG, "Received onStartCommand action $action")
}
when (action) {
"Start" -> {
val wm = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
fl = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Baresip")
val assets = arrayOf("accounts", "contacts", "config", "busy.wav", "callwaiting.wav",
"error.wav", "notfound.wav", "ring.wav", "ringback.wav")
var file = File(filesPath)
if (!file.exists()) {
Log.d(LOG_TAG, "Creating baresip directory")
try {
File(filesPath).mkdirs()
} catch (e: Error) {
Log.e(LOG_TAG, "Failed to create directory: " + e.toString())
}
}
for (a in assets) {
file = File("${filesPath}/$a")
if (!file.exists()) {
Log.d(LOG_TAG, "Copying asset $a")
Utils.copyAssetToFile(applicationContext, a, "$filesPath/$a")
} else {
Log.d(LOG_TAG, "Asset $a already copied")
if (a == "config") {
val inputStream: InputStream = file.inputStream()
var contents = inputStream.bufferedReader().use { it.readText() }
inputStream.close()
var write = false
if (!contents.contains("zrtp_hash")) {
contents = "${contents}zrtp_hash yes\n"
write = true
}
if (contents.contains(Regex("#module_app[ ]+mwi.so"))) {
contents = contents.replace(Regex("#module_app[ ]+mwi.so"),
"module_app mwi.so")
write = true
}
if (write) {
Log.d(LOG_TAG, "Writing $contents")
Utils.putFileContents(file, contents)
}
}
}
}
wl.acquire()
Thread(Runnable { baresipStart(filesPath) }).start()
BaresipService.isServiceRunning = true
registerReceiver(nr, IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"))
showStatusNotification()
}
"UpdateNotification" -> {
updateStatusNotification()
}
"Stop" -> {
stop()
}
"Kill" -> {
restarting = true
stop()
}
}
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
Log.d(LOG_TAG, "In onDestroy")
super.onDestroy()
Log.i(LOG_TAG, "Restart baresip killed by Android")
restarting = true
stop()
}
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val defaultChannel = NotificationChannel(DEFAULT_CHANNEL_ID, "Default",
NotificationManager.IMPORTANCE_LOW)
defaultChannel.description = "Tells that baresip is running"
defaultChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
nm.createNotificationChannel(defaultChannel)
val highChannel = NotificationChannel(HIGH_CHANNEL_ID, "High",
NotificationManager.IMPORTANCE_HIGH)
highChannel.description = "Tells about incoming call or message"
highChannel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
highChannel.enableVibration(true)
nm.createNotificationChannel(highChannel)
}
}
private fun showStatusNotification() {
snb.setVisibility(VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_stat)
.setContentIntent(npi)
.setOngoing(true)
.setContent(RemoteViews(packageName, R.layout.status_notification))
startForeground(STATUS_NOTIFICATION_ID, snb.build())
}
@Keep
fun uaAdd(uap: String) {
Log.d(LOG_TAG, "uaAdd at BaresipService")
val ua = UserAgent(uap)
uas.add(ua)
if (Api.ua_isregistered(uap)) {
Log.d(LOG_TAG, "Ua ${ua.account.aor} is registered")
status.add(R.drawable.dot_green)
} else {
Log.d(LOG_TAG, "Ua ${ua.account.aor} is NOT registered")
status.add(R.drawable.dot_yellow)
}
val intent = Intent("service event")
intent.putExtra("event", "ua added")
intent.putExtra("params", arrayListOf(uap))
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
updateStatusNotification()
}
@Keep
fun uaEvent(event: String, uap: String, callp: String) {
Log.d(LOG_TAG, "uaEvent got event $event/$uap/$callp")
if (!isServiceRunning) return
if (event == "exit")
return
val ua = UserAgent.find(uap)
if (ua == null) {
Log.w(LOG_TAG, "uaEvent did not find ua $uap")
return
}
val aor = ua.account.aor
var newEvent: String? = null
val ev = event.split(",")
for (account_index in uas.indices) {
if (uas[account_index].account.aor == aor) {
when (ev[0]) {
"registering" -> {
return
}
"registered" -> {
if (ua.account.regint == 0)
status[account_index] = R.drawable.dot_yellow
else
status[account_index] = R.drawable.dot_green
updateStatusNotification()
}
"registering failed" -> {
status[account_index] = R.drawable.dot_red
updateStatusNotification()
}
"unregistering" -> {
status[account_index] = R.drawable.dot_yellow
updateStatusNotification()
}
"call incoming" -> {
val peerUri = Api.call_peeruri(callp)
if (Call.calls().size > 0) {
Log.i(LOG_TAG, "Auto-rejecting incoming call $uap/$callp/$peerUri")
Api.ua_hangup(uap, callp, 486, "Busy Here")
CallHistory.add(CallHistory(aor, peerUri, "in", false))
CallHistory.save(filesPath)
ua.account.missedCalls = true
newEvent = "call rejected"
} else {
Log.d(LOG_TAG, "Incoming call $uap/$callp/$peerUri")
calls.add(Call(callp, ua, peerUri, "in", "incoming",
Utils.dtmfWatcher(callp)))
startRinging()
}
if ((newEvent == null) && !Utils.isVisible()) {
val cnb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
val caller = Utils.friendlyUri(ContactsActivity.contactName(peerUri),
Utils.aorDomain(aor))
val title = "Incoming call from $caller"
cnb.setSmallIcon(R.drawable.ic_stat)
.setColor(0x0ca1fd)
.setContentIntent(npi)
.setAutoCancel(true)
.setContentTitle(title)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
cnb.setVibrate(LongArray(0))
.setVisibility(VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH)
}
val answerIntent = Intent(this, MainActivity::class.java)
answerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
answerIntent.putExtra("action", "answer")
answerIntent.putExtra("callp", callp)
val answerPendingIntent = PendingIntent.getActivity(this,
0, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val rejectIntent = Intent(this, MainActivity::class.java)
rejectIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP)
rejectIntent.putExtra("action", "reject")
rejectIntent.putExtra("callp", callp)
val rejectPendingIntent = PendingIntent.getActivity(this,
1, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT)
cnb.addAction(R.drawable.ic_stat, "Answer", answerPendingIntent)
cnb.addAction(R.drawable.ic_stat, "Reject", rejectPendingIntent)
nm.notify(CALL_NOTIFICATION_ID, cnb.build())
}
}
"call established" -> {
nm.cancel(CALL_NOTIFICATION_ID)
val call = Call.find(callp)
if (call == null) {
Log.w(LOG_TAG, "Established AoR $aor call $callp is not found")
return
}
Log.d("Baresip", "AoR $aor call $callp established")
call.status = "connected"
call.onhold = false
call.security = R.drawable.box_red
CallHistory.add(CallHistory(aor, call.peerURI, call.dir, true))
CallHistory.save(filesPath)
call.hasHistory = true
if (rtTimer != null) {
rtTimer!!.cancel()
rtTimer = null
if (rt.isPlaying) rt.stop()
}
am.mode = AudioManager.MODE_IN_COMMUNICATION
requestAudioFocus( AudioManager.STREAM_VOICE_CALL)
am.isSpeakerphoneOn = false
}
"call transfer" -> {
val call = Call.find(callp)
if (call == null) {
Log.d("Baresip","AoR $aor call $callp to be transferred is not found")
return
}
if (!Utils.isVisible()) {
val cnb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
val target = Utils.friendlyUri(ContactsActivity.contactName(ev[1]),
Utils.aorDomain(aor))
val title = "Call transfer request to $target"
cnb.setSmallIcon(R.drawable.ic_stat)
.setColor(0x0ca1fd)
.setContentIntent(npi)
.setDefaults(Notification.DEFAULT_SOUND)
.setAutoCancel(true)
.setContentTitle(title)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
cnb.setVibrate(LongArray(0))
.setVisibility(VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH)
}
val acceptIntent = Intent(this, MainActivity::class.java)
acceptIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
acceptIntent.putExtra("action", "transfer")
acceptIntent.putExtra("uap", uap)
acceptIntent.putExtra("callp", callp)
acceptIntent.putExtra("uri", ev[1])
val acceptPendingIntent = PendingIntent.getActivity(this,
0, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val rejectIntent = Intent(this, MainActivity::class.java)
rejectIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP)
rejectIntent.putExtra("action", "deny")
rejectIntent.putExtra("callp", callp)
val rejectPendingIntent = PendingIntent.getActivity(this,
1, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT)
cnb.addAction(R.drawable.ic_stat, "Accept", acceptPendingIntent)
cnb.addAction(R.drawable.ic_stat, "Deny", rejectPendingIntent)
nm.notify(CALL_NOTIFICATION_ID, cnb.build())
return
}
}
"call closed" -> {
nm.cancel(CALL_NOTIFICATION_ID)
val call = Call.find(callp)
if (call == null) {
Log.d("Baresip","AoR $aor call $callp that is closed is not found")
return
}
Log.d("Baresip", "AoR $aor call $callp is closed")
stopRinging()
calls.remove(call)
if (!call.hasHistory) {
CallHistory.add(CallHistory(aor, call.peerURI, call.dir, false))
CallHistory.save(filesPath)
if (call.dir == "in") ua.account.missedCalls = true
}
if (Call.calls().size == 0) {
am.mode = AudioManager.MODE_NORMAL
} else {
val uaCalls = Call.uaCalls(ua, "")
if (uaCalls.size > 0)
Api.call_start_audio(uaCalls[uaCalls.size - 1].callp)
}
if (am.isSpeakerphoneOn) am.isSpeakerphoneOn = false
if (audioFocused) abandonAudioFocus()
}
"transfer failed" -> {
Log.d(LOG_TAG, "AoR $aor hanging up call $callp with ${ev[1]}")
Api.ua_hangup(uap, callp, 0, "")
return
}
}
}
}
if (newEvent == null) newEvent = event
val intent = Intent("service event")
intent.putExtra("event", newEvent)
intent.putExtra("params", arrayListOf(uap, callp))
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
@Keep
fun messageEvent(uap: String, peer: String, msg: ByteArray) {
var s = "Decoding of message failed!"
try {
s = String(msg, StandardCharsets.UTF_8)
} catch (e: Exception) {
Log.e(LOG_TAG, "UTF-8 decode failed")
}
Log.d(LOG_TAG, "Message event for $uap from $peer")
val ua = UserAgent.find(uap)
if (ua == null) {
Log.w(LOG_TAG, "messageEvent did not find ua $uap")
return
}
val timeStamp = System.currentTimeMillis().toString()
if (!Utils.isVisible()) {
val cnb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
val sender = Utils.friendlyUri(ContactsActivity.contactName(peer),
Utils.aorDomain(ua.account.aor))
cnb.setSmallIcon(R.drawable.ic_stat)
.setColor(0x0ca1fd)
.setContentIntent(npi)
.setDefaults(Notification.DEFAULT_SOUND)
.setAutoCancel(true)
.setContentTitle("Message from $sender")
.setContentText(s)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
cnb.setVibrate(LongArray(0))
.setVisibility(VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH)
}
val replyIntent = Intent(this, MainActivity::class.java)
replyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
replyIntent.putExtra("action", "reply")
replyIntent.putExtra("uap", uap)
replyIntent.putExtra("peer", peer)
val replyPendingIntent = PendingIntent.getActivity(this,
0, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val saveIntent = Intent(this, MainActivity::class.java)
saveIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP)
saveIntent.putExtra("action", "save")
saveIntent.putExtra("uap", uap)
saveIntent.putExtra("time", timeStamp)
val savePendingIntent = PendingIntent.getActivity(this,
1, saveIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val deleteIntent = Intent(this, MainActivity::class.java)
deleteIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP)
deleteIntent.putExtra("action", "delete")
deleteIntent.putExtra("uap", uap)
deleteIntent.putExtra("time", timeStamp)
val deletePendingIntent = PendingIntent.getActivity(this,
2, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
cnb.addAction(R.drawable.ic_stat, "Reply", replyPendingIntent)
cnb.addAction(R.drawable.ic_stat, "Save", savePendingIntent)
cnb.addAction(R.drawable.ic_stat, "Delete", deletePendingIntent)
nm.notify(MESSAGE_NOTIFICATION_ID, cnb.build())
}
val intent = Intent("service event")
intent.putExtra("event", "message")
intent.putExtra("params", arrayListOf(uap, peer, s, timeStamp))
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
@Keep
fun messageResponse(responseCode: Int, time: String) {
Log.d(LOG_TAG, "Message response $responseCode at $time")
val intent = Intent("message response")
intent.putExtra("response code", responseCode)
intent.putExtra("time", time)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
@Keep
fun stopped() {
Log.d(LOG_TAG, "got event 'stopped'")
isServiceRunning = false
val intent = Intent("service event")
intent.putExtra("event", "stopped")
intent.putExtra("params", arrayListOf<String>())
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
stopForeground(true)
stopSelf()
if (restarting) restart()
}
private fun updateStatusNotification() {
val contentView = RemoteViews(getPackageName(), R.layout.status_notification)
for (i: Int in 0 .. 5) {
val resID = resources.getIdentifier("status$i", "id", packageName)
if (i < status.size) {
contentView.setImageViewResource(resID, status[i])
contentView.setViewVisibility(resID, View.VISIBLE)
} else {
contentView.setViewVisibility(resID, View.INVISIBLE)
}
}
if (status.size > 4)
contentView.setViewVisibility(R.id.etc, View.VISIBLE)
else
contentView.setViewVisibility(R.id.etc, View.INVISIBLE)
snb.setContent(contentView)
nm.notify(STATUS_NOTIFICATION_ID, snb.build())
}
private fun requestAudioFocus(streamType: Int) {
val res: Int
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) && (audioFocusRequest == null)) {
val playbackAttributes = AudioAttributes.Builder()
.setLegacyStreamType(streamType)
.build()
@TargetApi(26)
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
.setAudioAttributes(playbackAttributes)
.build()
@TargetApi(26)
res = am.requestAudioFocus(audioFocusRequest)
if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d("Baresip", "Audio focus granted")
} else {
Log.d("Baresip", "Audio focus denied")
audioFocusRequest = null
}
} else {
res = am.requestAudioFocus(null, streamType,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d("Baresip", "Audio focus granted")
audioFocused = true
} else {
Log.d("Baresip", "Audio focus denied")
audioFocused = false
}
}
}
private fun abandonAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (audioFocusRequest != null) {
am.abandonAudioFocusRequest(audioFocusRequest)
audioFocusRequest = null
}
} else {
if (audioFocused) {
am.abandonAudioFocus(null)
audioFocused = false
}
}
}
private fun startRinging() {
am.mode = AudioManager.MODE_RINGTONE
requestAudioFocus(AudioManager.STREAM_RING)
rt.play()
rtTimer = Timer()
rtTimer!!.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
if (!rt.isPlaying()) {
rt.play()
}
}
}, 1000 * 1, 1000 * 1)
}
private fun stopRinging() {
if (rtTimer != null) {
rtTimer!!.cancel()
rtTimer = null
if (rt.isPlaying) rt.stop()
}
}
private fun stop() {
uas.clear()
status.clear()
history.clear()
messages.clear()
try {
unregisterReceiver(nr)
} catch (e: Exception) {
Log.d(LOG_TAG, "Receiver nr has not been registered")
}
if (this::nm.isInitialized) nm.cancelAll()
if (this::wl.isInitialized && wl.isHeld) wl.release()
if (this::fl.isInitialized && fl.isHeld) fl.release()
if (isServiceRunning) {
baresipStop(forceStop)
if (!forceStop) forceStop = true
} else if (restarting)
restart()
}
private fun restart() {
restarting = false
val broadcastIntent = Intent("com.tutpro.baresip.Restart")
sendBroadcast(broadcastIntent)
}
external fun baresipStart(path: String)
external fun baresipStop(force: Boolean)
companion object {
val STATUS_NOTIFICATION_ID = 101
val CALL_NOTIFICATION_ID = 102
val MESSAGE_NOTIFICATION_ID = 103
val DEFAULT_CHANNEL_ID = "com.tutpro.baresip.default"
val HIGH_CHANNEL_ID = "com.tutpro.baresip.high"
var isServiceRunning = false
var disconnected = false
var forceStop = false
var libraryLoaded = false
var uas = ArrayList<UserAgent>()
var status = ArrayList<Int>()
var calls = ArrayList<Call>()
var history = ArrayList<CallHistory>()
var messages = ArrayList<Message>()
var contacts = ArrayList<Contact>()
var audioFocused = false
var audioFocusRequest: AudioFocusRequest? = null
}
init {
if (!libraryLoaded) {
Log.d("Baresip", "Loading baresip library")
System.loadLibrary("baresip")
libraryLoaded = true
}
}
}