Speaker Phone fixes and improvements

This commit is contained in:
Juha Heinanen
2026-04-26 17:33:57 +03:00
parent 960fc11c00
commit 86887a6e7f
5 changed files with 153 additions and 63 deletions
@@ -174,6 +174,8 @@ private val showAlert = mutableStateOf(false)
@Composable @Composable
private fun AudioContent(contentPadding: PaddingValues) { private fun AudioContent(contentPadding: PaddingValues) {
oldSpeakerPhone = Config.variable("speaker_phone") == "yes"
newSpeakerPhone = oldSpeakerPhone
oldAudioModules = Config.variables("module") oldAudioModules = Config.variables("module")
oldOpusBitrate = Config.variable("opus_bitrate") oldOpusBitrate = Config.variable("opus_bitrate")
oldOpusPacketLoss = Config.variable("opus_packet_loss") oldOpusPacketLoss = Config.variable("opus_packet_loss")
@@ -350,7 +352,7 @@ private fun SpeakerPhone() {
showAlert.value = true showAlert.value = true
}, },
fontSize = 18.sp) fontSize = 18.sp)
var speakerPhone by remember { mutableStateOf(BaresipService.speakerPhone) } var speakerPhone by remember { mutableStateOf(oldSpeakerPhone) }
Switch( Switch(
checked = speakerPhone, checked = speakerPhone,
onCheckedChange = { onCheckedChange = {
@@ -634,10 +636,8 @@ private fun checkOnClick(ctx: Context): Result {
} }
} }
if (newSpeakerPhone != BaresipService.speakerPhone) { if (newSpeakerPhone != oldSpeakerPhone) {
BaresipService.speakerPhone = newSpeakerPhone Config.replaceVariable("speaker_phone", if (newSpeakerPhone) "yes" else "no")
Config.replaceVariable("speaker_phone",
if (BaresipService.speakerPhone) "yes" else "no")
save = true save = true
} }
@@ -115,6 +115,7 @@ class BaresipService: Service() {
private var hotSpotReceiverRegistered = false private var hotSpotReceiverRegistered = false
private var isNotificationInCall = false private var isNotificationInCall = false
private var isServiceClean = false private var isServiceClean = false
private var cleanupRunnable: Runnable? = null
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
override fun onCreate() { override fun onCreate() {
@@ -781,17 +782,26 @@ class BaresipService: Service() {
"call outgoing" -> { "call outgoing" -> {
if (call!!.status.value == "transferring") if (call!!.status.value == "transferring")
break break
if (Config.variable("speaker_phone") == "yes") {
Log.d(TAG, "Auto-on speakerphone for outgoing call")
speakerPhone = true
}
stopMediaPlayer() stopMediaPlayer()
setCallVolume() setCallVolume()
ensureCommunicationMode()
proximitySensing(proximitySensing) proximitySensing(proximitySensing)
} }
"call ringing" -> { "call ringing" -> {
if (Config.variable("speaker_phone") == "yes")
speakerPhone = true
ConnectionService.connections[callp]?.setRinging() ConnectionService.connections[callp]?.setRinging()
ensureCommunicationMode() ensureCommunicationMode()
playRingBack() playRingBack()
return return
} }
"call progress" -> { "call progress" -> {
if (Config.variable("speaker_phone") == "yes")
speakerPhone = true
ensureCommunicationMode() ensureCommunicationMode()
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
stopMediaPlayer() stopMediaPlayer()
@@ -851,6 +861,10 @@ class BaresipService: Service() {
return return
} }
"call incoming" -> { "call incoming" -> {
if (Config.variable("speaker_phone") == "yes") {
Log.d(TAG, "Auto-on speakerphone for incoming call")
speakerPhone = true
}
val peerUri = ev[1] val peerUri = ev[1]
Log.d(TAG, "Incoming call $uap/$callp/$peerUri") Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
if (Call.ofCallp(callp) == null) if (Call.ofCallp(callp) == null)
@@ -867,6 +881,8 @@ class BaresipService: Service() {
return return
} }
"call answered" -> { "call answered" -> {
if (Config.variable("speaker_phone") == "yes")
speakerPhone = true
stopMediaPlayer() stopMediaPlayer()
ensureCommunicationMode() ensureCommunicationMode()
if (call!!.status.value == "incoming") if (call!!.status.value == "incoming")
@@ -878,6 +894,8 @@ class BaresipService: Service() {
stopMediaPlayer() stopMediaPlayer()
} }
"call established" -> { "call established" -> {
if (Config.variable("speaker_phone") == "yes")
speakerPhone = true
ConnectionService.connections[callp]?.setActive() ConnectionService.connections[callp]?.setActive()
ensureCommunicationMode() ensureCommunicationMode()
nm.cancel(CALL_NOTIFICATION_ID) nm.cancel(CALL_NOTIFICATION_ID)
@@ -1929,52 +1947,83 @@ class BaresipService: Service() {
} }
if (Call.inCall() && isAnyCallMode) { if (Call.inCall() && isAnyCallMode) {
cleanupRunnable?.let {
Log.d(TAG, "Canceling pending speakerphone cleanup because call is active")
Handler(Looper.getMainLooper()).removeCallbacks(it)
cleanupRunnable = null
}
if (isSpeakerphoneOn == speakerPhone) { if (isSpeakerphoneOn == speakerPhone) {
Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.") Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.")
return return
} }
} else if (!Call.inCall() && currentMode == MODE_NORMAL) { } else if (!Call.inCall() && currentMode == MODE_NORMAL) {
if (speakerPhone) {
Log.d(TAG, "Resetting speakerPhone UI state while idle")
speakerPhone = false
postServiceEvent(
ServiceEvent(
"speaker update,false",
arrayListOf(0L, 0L),
System.nanoTime()
)
)
}
return return
} }
Log.d(TAG, "Scheduling ensureCommunicationMode (current: $currentMode) in 500ms") Log.d(TAG, "Scheduling ensureCommunicationMode (current: $currentMode) in 500ms (target speaker: $speakerPhone)")
Handler(Looper.getMainLooper()).postDelayed({ val handler = Handler(Looper.getMainLooper())
cleanupRunnable?.let { handler.removeCallbacks(it) }
val runnable = Runnable {
cleanupRunnable = null
if (Call.inCall()) { if (Call.inCall()) {
if (am.mode != MODE_IN_COMMUNICATION && am.mode != AudioManager.MODE_IN_CALL) { if (am.mode != MODE_IN_COMMUNICATION && am.mode != AudioManager.MODE_IN_CALL) {
am.mode = MODE_IN_COMMUNICATION am.mode = MODE_IN_COMMUNICATION
Log.d(TAG, "Manual Mode Guard (SDK ${VERSION.SDK_INT}): Setting MODE_IN_COMMUNICATON from ${am.mode}") Log.d(TAG, "Manual Mode Guard: Setting MODE_IN_COMMUNICATON from ${am.mode}")
}
Log.d(TAG, "Applying speakerphone state: $speakerPhone")
if (!Call.calls().any { ConnectionService.connections.containsKey(it.callp) }) {
Log.d(TAG, "No Telecom connection, using AudioManager for speaker")
Utils.setSpeakerPhone(mainExecutor, am, speakerPhone)
} else {
for (c in Call.calls()) {
ConnectionService.setOutput(c.callp, speakerPhone)
}
} }
Utils.setSpeakerPhone(mainExecutor, am, speakerPhone)
} else { } else {
if (am.mode != MODE_NORMAL) { if (am.mode != MODE_NORMAL) {
am.mode = MODE_NORMAL am.mode = MODE_NORMAL
Log.d(TAG, "Manual Mode Guard (SDK ${VERSION.SDK_INT}): Resetting to MODE_NORMAL") Log.d(TAG, "Manual Mode Guard: Resetting to MODE_NORMAL")
Utils.clearCommunicationDevice(am) Utils.clearCommunicationDevice(am)
if (speakerPhone) {
speakerPhone = false
postServiceEvent(
ServiceEvent(
"speaker update,false",
arrayListOf(0L, 0L),
System.nanoTime()
)
)
}
if (isMicMuted) {
isMicMuted = false
postServiceEvent(
ServiceEvent(
"mic muted,false",
arrayListOf(0L, 0L),
System.nanoTime()
)
)
}
resetCallVolume()
proximitySensing(false)
} }
if (speakerPhone) {
Log.d(TAG, "Resetting speakerPhone runtime state after call")
speakerPhone = false
postServiceEvent(
ServiceEvent(
"speaker update,false",
arrayListOf(0L, 0L),
System.nanoTime()
)
)
}
if (isMicMuted) {
isMicMuted = false
postServiceEvent(
ServiceEvent(
"mic muted,false",
arrayListOf(0L, 0L),
System.nanoTime()
)
)
}
resetCallVolume()
proximitySensing(false)
} }
}, 500) }
cleanupRunnable = runnable
handler.postDelayed(runnable, 500)
} }
@SuppressLint("WakelockTimeout", "Wakelock") @SuppressLint("WakelockTimeout", "Wakelock")
@@ -2207,6 +2256,7 @@ class BaresipService: Service() {
var isConfigInitialized = false var isConfigInitialized = false
var libraryLoaded = false var libraryLoaded = false
var callVolume = 0 var callVolume = 0
@Volatile
var speakerPhone = false var speakerPhone = false
var audioDelay = if (VERSION.SDK_INT < 31) 1500L else 500L var audioDelay = if (VERSION.SDK_INT < 31) 1500L else 500L
var dynDns = false var dynDns = false
@@ -177,10 +177,10 @@ object Config {
val speakerPhone = previousVariable("speaker_phone") val speakerPhone = previousVariable("speaker_phone")
if (speakerPhone != "") { if (speakerPhone != "") {
config = "${config}speaker_phone $speakerPhone\n" config = "${config}speaker_phone $speakerPhone\n"
BaresipService.speakerPhone = speakerPhone == "yes"
} else { } else {
config = "${config}speaker_phone no\n" config = "${config}speaker_phone no\n"
} }
BaresipService.speakerPhone = false
val previousModules = previousVariables("module") val previousModules = previousVariables("module")
for (module in audioModules) for (module in audioModules)
@@ -35,6 +35,14 @@ class ConnectionService : ConnectionService() {
connections.remove(callp) connections.remove(callp)
} }
} }
fun setOutput(callp: Long, speaker: Boolean) {
connections[callp]?.let {
Log.d(TAG, "Setting audio route for $callp to speaker=$speaker")
@Suppress("DEPRECATION")
it.setAudioRoute(if (speaker) CallAudioState.ROUTE_SPEAKER else CallAudioState.ROUTE_EARPIECE)
}
}
} }
override fun onCreateIncomingConnection( override fun onCreateIncomingConnection(
@@ -62,9 +70,16 @@ class ConnectionService : ConnectionService() {
val ua = UserAgent.ofUap(uap) val ua = UserAgent.ofUap(uap)
if (ua != null) { if (ua != null) {
if (BaresipService.speakerPhone) { if (BaresipService.speakerPhone || Config.variable("speaker_phone") == "yes") {
BaresipService.speakerPhone = true
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER) connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER)
BaresipService.postServiceEvent(
ServiceEvent("speaker update,true",
arrayListOf(uap, callp),
System.nanoTime()
)
)
} }
if (ua.account.answerMode == Api.ANSWERMODE_AUTO) { if (ua.account.answerMode == Api.ANSWERMODE_AUTO) {
Log.d(TAG, "Auto-answering call $callp") Log.d(TAG, "Auto-answering call $callp")
@@ -107,9 +122,16 @@ class ConnectionService : ConnectionService() {
val connection = BaresipConnection(uap, 0L) val connection = BaresipConnection(uap, 0L)
pendingOutgoingConnection = connection pendingOutgoingConnection = connection
if (BaresipService.speakerPhone) { if (BaresipService.speakerPhone || Config.variable("speaker_phone") == "yes") {
BaresipService.speakerPhone = true
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER) connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER)
BaresipService.postServiceEvent(
ServiceEvent("speaker update,true",
arrayListOf(uap, 0L),
System.nanoTime()
)
)
} }
connection.setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED) connection.setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED)
@@ -218,15 +240,25 @@ class ConnectionService : ConnectionService() {
) )
} }
val isSpeaker = it.route == CallAudioState.ROUTE_SPEAKER val isSpeaker = it.route == CallAudioState.ROUTE_SPEAKER
if (BaresipService.speakerPhone != isSpeaker) { val call = Call.ofCallp(callp)
BaresipService.speakerPhone = isSpeaker val status = call?.status?.value ?: "idle"
BaresipService.postServiceEvent( val hasPendingOrActiveConnection = connections.isNotEmpty() || pendingOutgoingConnection != null
ServiceEvent("speaker update,${isSpeaker}", if (isSpeaker != BaresipService.speakerPhone && (status != "connected" || hasPendingOrActiveConnection)) {
arrayListOf(uap, callp), if (status != "connected") {
System.nanoTime() Log.d(TAG, "Suppressing speaker update,$isSpeaker during call setup (status=$status, intent=${BaresipService.speakerPhone})")
) return@let
) }
} }
if (status == "connected" && BaresipService.speakerPhone != isSpeaker) {
Log.d(TAG, "Syncing speakerPhone variable to hardware state: $isSpeaker")
BaresipService.speakerPhone = isSpeaker
}
BaresipService.postServiceEvent(
ServiceEvent("speaker update,$isSpeaker",
arrayListOf(uap, callp),
System.nanoTime()
)
)
} }
} }
@@ -212,19 +212,20 @@ private fun MainScreen(
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val ua = uas.value.find { it.account.aor == viewModel.selectedAor.value } val selectedAor by viewModel.selectedAor.collectAsState()
val ua = uas.value.find { it.account.aor == selectedAor }
val call = ua?.currentCall() val call = ua?.currentCall()
val showKeyboard by viewModel.showKeyboard.collectAsState() val showKeyboard by viewModel.showKeyboard.collectAsState()
val hideKeyboard by viewModel.hideKeyboard.collectAsState() val hideKeyboard by viewModel.hideKeyboard.collectAsState()
LaunchedEffect(showKeyboard) { LaunchedEffect(showKeyboard) {
if (viewModel.showKeyboard.value > 0) if (showKeyboard > 0)
keyboardController?.show() keyboardController?.show()
} }
LaunchedEffect(hideKeyboard) { LaunchedEffect(hideKeyboard) {
if (viewModel.hideKeyboard.value > 0) if (hideKeyboard > 0)
keyboardController?.hide() keyboardController?.hide()
} }
@@ -234,6 +235,7 @@ private fun MainScreen(
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
Log.d(TAG, "Resumed to MainScreen") Log.d(TAG, "Resumed to MainScreen")
BaresipService.isMainVisible = true BaresipService.isMainVisible = true
viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone)
viewModel.updateCalls(Call.calls().toList()) viewModel.updateCalls(Call.calls().toList())
(Call.call("incoming") ?: Call.calls().lastOrNull())?.let { (Call.call("incoming") ?: Call.calls().lastOrNull())?.let {
spinToAor(viewModel, it.ua.account.aor) spinToAor(viewModel, it.ua.account.aor)
@@ -524,8 +526,6 @@ private fun TopAppBar(
val ctx = LocalContext.current val ctx = LocalContext.current
val currentMicIcon by viewModel.micIcon.collectAsState() val currentMicIcon by viewModel.micIcon.collectAsState()
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val recOffImage = Icons.Filled.VoiceOverOff val recOffImage = Icons.Filled.VoiceOverOff
val recOnImage = Icons.Filled.RecordVoiceOver val recOnImage = Icons.Filled.RecordVoiceOver
var isRecOn by remember { mutableStateOf(BaresipService.isRecOn) } var isRecOn by remember { mutableStateOf(BaresipService.isRecOn) }
@@ -648,7 +648,6 @@ private fun TopAppBar(
.clip(CircleShape) .clip(CircleShape)
.combinedClickable( .combinedClickable(
onClick = { onClick = {
val isCurrentlyOn = isSpeakerOn
val aor = viewModel.selectedAor.value val aor = viewModel.selectedAor.value
val ua = uas.value.find { it.account.aor == aor } val ua = uas.value.find { it.account.aor == aor }
val call = ua?.currentCall() val call = ua?.currentCall()
@@ -657,13 +656,14 @@ private fun TopAppBar(
if (connection != null) { if (connection != null) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
connection.setAudioRoute( connection.setAudioRoute(
if (isCurrentlyOn) if (isSpeakerOn)
android.telecom.CallAudioState.ROUTE_EARPIECE android.telecom.CallAudioState.ROUTE_EARPIECE
else else
android.telecom.CallAudioState.ROUTE_SPEAKER android.telecom.CallAudioState.ROUTE_SPEAKER
) )
} else { } else {
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am) BaresipService.speakerPhone = !isSpeakerOn
viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone)
} }
}, },
onLongClick = { onLongClick = {
@@ -2291,14 +2291,32 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
return return
} }
val ev = event.split(",")
val uap = params[0] as Long val uap = params[0] as Long
when (ev[0]) {
"mic muted" -> {
val muted = ev[1].toBoolean()
if (muted)
viewModel.updateMicIcon(Icons.Filled.MicOff)
else
viewModel.updateMicIcon(Icons.Filled.Mic)
handleNextEvent()
return
}
"speaker update" -> {
viewModel.updateSpeakerPhoneStatus(ev[1].toBoolean())
handleNextEvent()
return
}
}
val ua = UserAgent.ofUap(uap) val ua = UserAgent.ofUap(uap)
if (ua == null) { if (ua == null) {
handleNextEvent("handleServiceEvent '$event' did not find ua $uap") handleNextEvent("handleServiceEvent '$event' did not find ua $uap")
return return
} }
val ev = event.split(",")
Log.d(TAG, "Handling service event '${ev[0]}' for $uap") Log.d(TAG, "Handling service event '${ev[0]}' for $uap")
val acc = ua.account val acc = ua.account
val aor = ua.account.aor val aor = ua.account.aor
@@ -2480,16 +2498,6 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
if (aor == viewModel.selectedAor.value) if (aor == viewModel.selectedAor.value)
viewModel.triggerAccountUpdate() viewModel.triggerAccountUpdate()
} }
"mic muted" -> {
val muted = ev[1].toBoolean()
if (muted)
viewModel.updateMicIcon(Icons.Filled.MicOff)
else
viewModel.updateMicIcon(Icons.Filled.Mic)
}
"speaker update" -> {
viewModel.updateSpeakerPhoneStatus(ev[1].toBoolean())
}
else -> Log.e(TAG, "Unknown event '${ev[0]}'") else -> Log.e(TAG, "Unknown event '${ev[0]}'")
} }