Merge branch 'calling_app'
This commit is contained in:
@@ -177,9 +177,6 @@ private var password = mutableStateOf("")
|
||||
private val selectItems = mutableStateOf(listOf<String>())
|
||||
private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
|
||||
private val showSelectItemDialog = mutableStateOf(false)
|
||||
private var callRunnable: Runnable? = null
|
||||
private var callHandler: Handler = Handler(Looper.getMainLooper())
|
||||
private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null
|
||||
|
||||
fun NavGraphBuilder.mainScreenRoute(
|
||||
navController: NavController,
|
||||
@@ -529,7 +526,7 @@ private fun TopAppBar(
|
||||
val recOffImage = Icons.Filled.VoiceOverOff
|
||||
val recOnImage = Icons.Filled.RecordVoiceOver
|
||||
var isRecOn by remember { mutableStateOf(BaresipService.isRecOn) }
|
||||
val isSpeakerOn = remember { mutableStateOf(Utils.isSpeakerPhoneOn(am)) }
|
||||
val isSpeakerOn by viewModel.isSpeakerOn.collectAsState()
|
||||
var menuExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val about = stringResource(R.string.about)
|
||||
@@ -648,11 +645,22 @@ private fun TopAppBar(
|
||||
.clip(CircleShape)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (Build.VERSION.SDK_INT >= 31)
|
||||
Log.d(TAG, "Toggling speakerphone when dev/mode is " +
|
||||
"${am.communicationDevice!!.type}/${am.mode}")
|
||||
isSpeakerOn.value = !Utils.isSpeakerPhoneOn(am)
|
||||
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
|
||||
val isCurrentlyOn = isSpeakerOn
|
||||
val aor = viewModel.selectedAor.value
|
||||
val ua = uas.value.find { it.account.aor == aor }
|
||||
val call = ua?.currentCall()
|
||||
val connection = if (call != null) ConnectionService.connections[call.callp] else null
|
||||
if (connection != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
connection.setAudioRoute(
|
||||
if (isCurrentlyOn)
|
||||
android.telecom.CallAudioState.ROUTE_EARPIECE
|
||||
else
|
||||
android.telecom.CallAudioState.ROUTE_SPEAKER
|
||||
)
|
||||
} else {
|
||||
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
alertTitle.value = speakerPhoneTitle
|
||||
@@ -664,7 +672,7 @@ private fun TopAppBar(
|
||||
Icon(
|
||||
imageVector = Icons.Filled.SpeakerPhone,
|
||||
contentDescription = null,
|
||||
tint = if (isSpeakerOn.value)
|
||||
tint = if (isSpeakerOn)
|
||||
MaterialTheme.colorScheme.error
|
||||
else
|
||||
MaterialTheme.colorScheme.onPrimary,
|
||||
@@ -884,8 +892,7 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
|
||||
|
||||
val calls by viewModel.calls.collectAsState()
|
||||
val selectedAor by viewModel.selectedAor.collectAsState()
|
||||
val filteredCalls = calls.filter { it.ua.account.aor == selectedAor }
|
||||
|
||||
val filteredCalls = calls.filter { it.ua.account.aor == selectedAor && it.status.value != "disconnecting" }
|
||||
val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" }
|
||||
val conferenceCall = filteredCalls.any { it.conferenceCall }
|
||||
|
||||
@@ -1483,12 +1490,13 @@ private fun CallRow(
|
||||
onClick = {
|
||||
if (call.terminated.value) return@IconButton
|
||||
call.terminated.value = true
|
||||
abandonAudioFocus(ctx)
|
||||
Log.d(
|
||||
TAG,
|
||||
"AoR ${call.ua.account.aor} canceling call ${call.callp} with ${call.callUri.value}"
|
||||
)
|
||||
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
|
||||
val connection = ConnectionService.connections[call.callp]
|
||||
if (connection != null)
|
||||
connection.onDisconnect()
|
||||
else {
|
||||
Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}")
|
||||
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
@@ -1509,9 +1517,13 @@ private fun CallRow(
|
||||
onClick = {
|
||||
if (call.terminated.value) return@IconButton
|
||||
call.terminated.value = true
|
||||
abandonAudioFocus(ctx)
|
||||
Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp} with ${call.callUri.value}")
|
||||
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
|
||||
val connection = ConnectionService.connections[call.callp]
|
||||
if (connection != null)
|
||||
connection.onDisconnect()
|
||||
else {
|
||||
Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}")
|
||||
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
@@ -1522,34 +1534,42 @@ private fun CallRow(
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(48.dp),
|
||||
onClick = {
|
||||
if (call.onhold) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"AoR ${call.ua.account.aor} resuming call ${call.callp} with ${call.callUri.value}"
|
||||
)
|
||||
call.resume()
|
||||
} else {
|
||||
Log.d(
|
||||
TAG,
|
||||
"AoR ${call.ua.account.aor} holding call ${call.callp} with ${call.callUri.value}"
|
||||
)
|
||||
call.hold()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PauseCircle,
|
||||
modifier = Modifier.size(42.dp),
|
||||
tint = if (call.callOnHold.value)
|
||||
MaterialTheme.colorScheme.error
|
||||
else
|
||||
MaterialTheme.colorScheme.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
if (!call.conferenceCall)
|
||||
IconButton(
|
||||
modifier = Modifier.size(48.dp),
|
||||
onClick = {
|
||||
val connection = ConnectionService.connections[call.callp]
|
||||
if (call.onhold) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"AoR ${call.ua.account.aor} resuming call ${call.callp} with ${call.callUri.value}"
|
||||
)
|
||||
if (connection != null)
|
||||
connection.onUnhold()
|
||||
else
|
||||
call.resume()
|
||||
} else {
|
||||
Log.d(
|
||||
TAG,
|
||||
"AoR ${call.ua.account.aor} holding call ${call.callp} with ${call.callUri.value}"
|
||||
)
|
||||
if (connection != null)
|
||||
connection.onHold()
|
||||
else
|
||||
call.hold()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PauseCircle,
|
||||
modifier = Modifier.size(42.dp),
|
||||
tint = if (call.callOnHold.value)
|
||||
MaterialTheme.colorScheme.error
|
||||
else
|
||||
MaterialTheme.colorScheme.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
var showTransferDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -1559,12 +1579,22 @@ private fun CallRow(
|
||||
enabled = call.transferButtonEnabled.value,
|
||||
onClick = {
|
||||
if (call.onHoldCall != null) {
|
||||
if (!call.executeTransfer()) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = ctx.getString(R.string.transfer_failed)
|
||||
if (!Api.call_supported(call.callp, Api.REPLACES)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = ctx.getString(R.string.replaces_not_supported)
|
||||
showAlert.value = true
|
||||
}
|
||||
} else
|
||||
else {
|
||||
val connection = ConnectionService.connections[call.callp]
|
||||
connection?.onHold()
|
||||
if (!call.executeTransfer()) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = ctx.getString(R.string.transfer_failed)
|
||||
showAlert.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
showTransferDialog = true
|
||||
},
|
||||
) {
|
||||
@@ -1961,16 +1991,18 @@ private fun CallRow(
|
||||
|
||||
@Composable
|
||||
private fun OnHoldNotice() {
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
shape = RoundedCornerShape(20)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.call_is_on_hold),
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
shape = RoundedCornerShape(20)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.call_is_on_hold),
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2011,10 +2043,10 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean) {
|
||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
||||
val aor = ua.account.aor
|
||||
private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean,
|
||||
onHoldCallp: Long = 0L) {
|
||||
val aor = viewModel.selectedAor.value
|
||||
val ua = UserAgent.ofAor(aor)!!
|
||||
val peerUri = if (Utils.isTelNumber(uriText))
|
||||
"tel:$uriText"
|
||||
else
|
||||
@@ -2035,44 +2067,31 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer
|
||||
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
|
||||
showAlert.value = true
|
||||
}
|
||||
else if (Call.isAnyCallActive(ctx))
|
||||
else if (Utils.isPSTNCallActive(ctx) || Call.calls().any { it.ua.account.aor != ua.account.aor } )
|
||||
Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show()
|
||||
else if (!BaresipService.requestAudioFocus(ctx))
|
||||
Toast.makeText(ctx, R.string.audio_focus_denied, Toast.LENGTH_SHORT).show()
|
||||
else {
|
||||
viewModel.dialerState.callButtonsEnabled.value = false
|
||||
if (Build.VERSION.SDK_INT < 31) {
|
||||
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
|
||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
runCall(ctx, viewModel, ua, uri, conferenceCall)
|
||||
} else {
|
||||
if (am.mode == AudioManager.MODE_IN_COMMUNICATION) {
|
||||
runCall(ctx, viewModel, ua, uri, conferenceCall)
|
||||
} else {
|
||||
audioModeChangedListener = AudioManager.OnModeChangedListener { mode ->
|
||||
if (mode == AudioManager.MODE_IN_COMMUNICATION) {
|
||||
Log.d(TAG, "Audio mode changed to MODE_IN_COMMUNICATION using " +
|
||||
"device ${am.communicationDevice!!.type}")
|
||||
if (audioModeChangedListener != null) {
|
||||
am.removeOnModeChangedListener(audioModeChangedListener!!)
|
||||
audioModeChangedListener = null
|
||||
}
|
||||
runCall(ctx, viewModel, ua, uri, conferenceCall)
|
||||
} else {
|
||||
Log.d(TAG, "Audio mode changed to mode ${am.mode} using " +
|
||||
"device ${am.communicationDevice!!.type}")
|
||||
}
|
||||
}
|
||||
am.addOnModeChangedListener(ctx.mainExecutor, audioModeChangedListener!!)
|
||||
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
|
||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
}
|
||||
val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as android.telecom.TelecomManager
|
||||
val extras = android.os.Bundle()
|
||||
extras.putParcelable(android.telecom.TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
|
||||
BaresipService.getPhoneAccountHandle(ctx))
|
||||
val callExtras = android.os.Bundle()
|
||||
callExtras.putBoolean("conferenceCall", conferenceCall)
|
||||
callExtras.putLong("uap", ua.uap)
|
||||
if (onHoldCallp != 0L)
|
||||
callExtras.putLong("onHoldCallp", onHoldCallp)
|
||||
extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras)
|
||||
try {
|
||||
Log.d(TAG, "Placing Telecom call to $uri with uap=${ua.uap}")
|
||||
tm.placeCall(uri.toUri(), extras)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "placeCall failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun answer(ctx: Context, call: Call) {
|
||||
Log.d(TAG, "AoR ${call.ua.account.aor} answering call from ${call.callUri.value}")
|
||||
ConnectionService.connections[call.callp]?.setActive()
|
||||
val intent = Intent(ctx, BaresipService::class.java)
|
||||
intent.action = "Call Answer"
|
||||
intent.putExtra("uap", call.ua.uap)
|
||||
@@ -2082,59 +2101,12 @@ private fun answer(ctx: Context, call: Call) {
|
||||
|
||||
private fun reject(call: Call) {
|
||||
Log.d(TAG, "AoR ${call.ua.account.aor} rejecting call ${call.callp} from ${call.callUri.value}")
|
||||
call.rejected = true
|
||||
Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here")
|
||||
}
|
||||
|
||||
private fun runCall(ctx: Context, viewModel: ViewModel, ua: UserAgent, uri: String, conferenceCall: Boolean) {
|
||||
callRunnable = Runnable {
|
||||
callRunnable = null
|
||||
val newCall = call(ctx, viewModel, ua, uri, conferenceCall)
|
||||
if (newCall == null) {
|
||||
BaresipService.abandonAudioFocus(ctx)
|
||||
viewModel.dialerState.callButtonsEnabled.value = true
|
||||
}
|
||||
}
|
||||
callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay)
|
||||
}
|
||||
|
||||
private fun call(
|
||||
ctx: Context,
|
||||
viewModel: ViewModel,
|
||||
ua: UserAgent,
|
||||
uri: String,
|
||||
conferenceCall: Boolean,
|
||||
onHoldCall: Call? = null
|
||||
): Call? {
|
||||
spinToAor(viewModel, ua.account.aor)
|
||||
if (conferenceCall && ua.calls().isEmpty())
|
||||
Api.module_load("mixminus")
|
||||
val callp = ua.callAlloc(0L, Api.VIDMODE_OFF)
|
||||
return if (callp != 0L) {
|
||||
Log.d(TAG, "Adding outgoing call ${ua.uap}/$callp/$uri")
|
||||
val call = Call(callp, ua, uri, "out", "outgoing")
|
||||
call.onHoldCall = onHoldCall
|
||||
call.conferenceCall = conferenceCall
|
||||
call.add()
|
||||
if (onHoldCall != null)
|
||||
onHoldCall.newCall = call
|
||||
if (call.connect(uri)) {
|
||||
showCall(ctx, viewModel, ua)
|
||||
call
|
||||
} else {
|
||||
Log.w(TAG, "call_connect $callp failed")
|
||||
if (onHoldCall != null)
|
||||
onHoldCall.newCall = null
|
||||
call.remove()
|
||||
call.destroy()
|
||||
showCall(ctx, viewModel, ua)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "callAlloc for ${ua.uap} to $uri failed")
|
||||
if (conferenceCall && ua.calls().isEmpty())
|
||||
Api.module_unload("mixminus")
|
||||
null
|
||||
val connection = ConnectionService.connections[call.callp]
|
||||
if (connection != null)
|
||||
connection.onReject()
|
||||
else {
|
||||
call.rejected = true
|
||||
Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2152,13 +2124,23 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText:
|
||||
val call = ua.currentCall()
|
||||
if (call != null) {
|
||||
if (attended) {
|
||||
if (call.hold()) {
|
||||
val connection = ConnectionService.connections[call.callp]
|
||||
val success = if (connection != null) {
|
||||
connection.onHold()
|
||||
true
|
||||
} else {
|
||||
call.hold()
|
||||
}
|
||||
if (success) {
|
||||
call.onhold = true
|
||||
call.referTo = uri
|
||||
call(ctx, viewModel, ua, uri, false,call)
|
||||
makeCall(ctx, viewModel, uri, false, call.callp)
|
||||
showCall(ctx, viewModel, ua, call)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val connection = ConnectionService.connections[call.callp]
|
||||
connection?.onHold()
|
||||
if (!call.transfer(uri)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = ctx.getString(R.string.transfer_failed)
|
||||
@@ -2205,8 +2187,9 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
|
||||
call.focusDtmf.value = true
|
||||
viewModel.requestShowKeyboard()
|
||||
}
|
||||
Log.d(TAG, "Showing call ${call.callp} from ${call.ua.account.aor} with status ${call.status.value}")
|
||||
when (call.status.value) {
|
||||
"outgoing", "transferring", "answered" -> {
|
||||
"outgoing", "transferring", "answered", "disconnecting" -> {
|
||||
call.callUriLabel.value = if (call.status.value == "answered")
|
||||
ctx.getString(R.string.incoming_call_from_dots)
|
||||
else
|
||||
@@ -2373,6 +2356,9 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
|
||||
"call update" -> {
|
||||
showCall(ctx, viewModel, ua)
|
||||
}
|
||||
"update calls" -> {
|
||||
viewModel.updateCalls(Call.calls().toList())
|
||||
}
|
||||
"call verify" -> {
|
||||
val callp = params[1] as Long
|
||||
val call = Call.ofCallp(callp)
|
||||
@@ -2449,13 +2435,14 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
|
||||
val call = Call.ofCallp(callp)
|
||||
if (call in Call.calls())
|
||||
Api.ua_hangup(uap, callp, 487, "Request Terminated")
|
||||
call(ctx, viewModel, ua, ev[1], false)
|
||||
makeCall(ctx, viewModel, ev[1], false)
|
||||
showCall(ctx, viewModel, ua)
|
||||
}
|
||||
"transfer failed" -> {
|
||||
showCall(ctx, viewModel, ua)
|
||||
}
|
||||
"call closed" -> {
|
||||
viewModel.updateCalls(Call.calls().toList())
|
||||
val activity = ctx as? Activity
|
||||
if (activity != null) {
|
||||
val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
@@ -2490,6 +2477,16 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
|
||||
if (aor == viewModel.selectedAor.value)
|
||||
viewModel.triggerAccountUpdate()
|
||||
}
|
||||
"mic muted" -> {
|
||||
val muted = ev[1].toBoolean()
|
||||
if (muted)
|
||||
viewModel.updateMicIcon(Icons.Filled.MicOff)
|
||||
else
|
||||
viewModel.updateMicIcon(Icons.Filled.Mic)
|
||||
}
|
||||
"speaker" -> {
|
||||
viewModel.updateSpeakerPhoneStatus(ev[1].toBoolean())
|
||||
}
|
||||
else -> Log.e(TAG, "Unknown event '${ev[0]}'")
|
||||
}
|
||||
|
||||
@@ -2769,19 +2766,3 @@ private fun restore(ctx: Context, password: String, onRestartApp: () -> Unit) {
|
||||
downloadsOutputUri = null
|
||||
}
|
||||
|
||||
private fun abandonAudioFocus(ctx: Context) {
|
||||
if (Build.VERSION.SDK_INT < 31) {
|
||||
if (callRunnable != null) {
|
||||
callHandler.removeCallbacks(callRunnable!!)
|
||||
callRunnable = null
|
||||
BaresipService.abandonAudioFocus(ctx)
|
||||
}
|
||||
} else {
|
||||
if (audioModeChangedListener != null) {
|
||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.removeOnModeChangedListener(audioModeChangedListener!!)
|
||||
audioModeChangedListener = null
|
||||
BaresipService.abandonAudioFocus(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user