diff --git a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt index fea910ad..cedcf87e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt @@ -174,6 +174,8 @@ private val showAlert = mutableStateOf(false) @Composable private fun AudioContent(contentPadding: PaddingValues) { + oldSpeakerPhone = Config.variable("speaker_phone") == "yes" + newSpeakerPhone = oldSpeakerPhone oldAudioModules = Config.variables("module") oldOpusBitrate = Config.variable("opus_bitrate") oldOpusPacketLoss = Config.variable("opus_packet_loss") @@ -350,7 +352,7 @@ private fun SpeakerPhone() { showAlert.value = true }, fontSize = 18.sp) - var speakerPhone by remember { mutableStateOf(BaresipService.speakerPhone) } + var speakerPhone by remember { mutableStateOf(oldSpeakerPhone) } Switch( checked = speakerPhone, onCheckedChange = { @@ -634,10 +636,8 @@ private fun checkOnClick(ctx: Context): Result { } } - if (newSpeakerPhone != BaresipService.speakerPhone) { - BaresipService.speakerPhone = newSpeakerPhone - Config.replaceVariable("speaker_phone", - if (BaresipService.speakerPhone) "yes" else "no") + if (newSpeakerPhone != oldSpeakerPhone) { + Config.replaceVariable("speaker_phone", if (newSpeakerPhone) "yes" else "no") save = true } diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index ba17f211..25ee61aa 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -115,6 +115,7 @@ class BaresipService: Service() { private var hotSpotReceiverRegistered = false private var isNotificationInCall = false private var isServiceClean = false + private var cleanupRunnable: Runnable? = null @SuppressLint("WakelockTimeout") override fun onCreate() { @@ -781,17 +782,26 @@ class BaresipService: Service() { "call outgoing" -> { if (call!!.status.value == "transferring") break + if (Config.variable("speaker_phone") == "yes") { + Log.d(TAG, "Auto-on speakerphone for outgoing call") + speakerPhone = true + } stopMediaPlayer() setCallVolume() + ensureCommunicationMode() proximitySensing(proximitySensing) } "call ringing" -> { + if (Config.variable("speaker_phone") == "yes") + speakerPhone = true ConnectionService.connections[callp]?.setRinging() ensureCommunicationMode() playRingBack() return } "call progress" -> { + if (Config.variable("speaker_phone") == "yes") + speakerPhone = true ensureCommunicationMode() if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() @@ -851,6 +861,10 @@ class BaresipService: Service() { return } "call incoming" -> { + if (Config.variable("speaker_phone") == "yes") { + Log.d(TAG, "Auto-on speakerphone for incoming call") + speakerPhone = true + } val peerUri = ev[1] Log.d(TAG, "Incoming call $uap/$callp/$peerUri") if (Call.ofCallp(callp) == null) @@ -867,6 +881,8 @@ class BaresipService: Service() { return } "call answered" -> { + if (Config.variable("speaker_phone") == "yes") + speakerPhone = true stopMediaPlayer() ensureCommunicationMode() if (call!!.status.value == "incoming") @@ -878,6 +894,8 @@ class BaresipService: Service() { stopMediaPlayer() } "call established" -> { + if (Config.variable("speaker_phone") == "yes") + speakerPhone = true ConnectionService.connections[callp]?.setActive() ensureCommunicationMode() nm.cancel(CALL_NOTIFICATION_ID) @@ -1929,52 +1947,83 @@ class BaresipService: Service() { } 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) { Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.") return } } 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 } - Log.d(TAG, "Scheduling ensureCommunicationMode (current: $currentMode) in 500ms") - Handler(Looper.getMainLooper()).postDelayed({ + Log.d(TAG, "Scheduling ensureCommunicationMode (current: $currentMode) in 500ms (target speaker: $speakerPhone)") + val handler = Handler(Looper.getMainLooper()) + cleanupRunnable?.let { handler.removeCallbacks(it) } + + val runnable = Runnable { + cleanupRunnable = null if (Call.inCall()) { if (am.mode != MODE_IN_COMMUNICATION && am.mode != AudioManager.MODE_IN_CALL) { 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 { if (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) - 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") @@ -2207,6 +2256,7 @@ class BaresipService: Service() { var isConfigInitialized = false var libraryLoaded = false var callVolume = 0 + @Volatile var speakerPhone = false var audioDelay = if (VERSION.SDK_INT < 31) 1500L else 500L var dynDns = false diff --git a/app/src/main/kotlin/com/tutpro/baresip/Config.kt b/app/src/main/kotlin/com/tutpro/baresip/Config.kt index 4eb42578..492afb96 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Config.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Config.kt @@ -177,10 +177,10 @@ object Config { val speakerPhone = previousVariable("speaker_phone") if (speakerPhone != "") { config = "${config}speaker_phone $speakerPhone\n" - BaresipService.speakerPhone = speakerPhone == "yes" } else { config = "${config}speaker_phone no\n" } + BaresipService.speakerPhone = false val previousModules = previousVariables("module") for (module in audioModules) diff --git a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt index 383a4240..b91e291b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt @@ -35,6 +35,14 @@ class ConnectionService : ConnectionService() { 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( @@ -62,9 +70,16 @@ class ConnectionService : ConnectionService() { val ua = UserAgent.ofUap(uap) if (ua != null) { - if (BaresipService.speakerPhone) { + if (BaresipService.speakerPhone || Config.variable("speaker_phone") == "yes") { + BaresipService.speakerPhone = true @Suppress("DEPRECATION") connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER) + BaresipService.postServiceEvent( + ServiceEvent("speaker update,true", + arrayListOf(uap, callp), + System.nanoTime() + ) + ) } if (ua.account.answerMode == Api.ANSWERMODE_AUTO) { Log.d(TAG, "Auto-answering call $callp") @@ -107,9 +122,16 @@ class ConnectionService : ConnectionService() { val connection = BaresipConnection(uap, 0L) pendingOutgoingConnection = connection - if (BaresipService.speakerPhone) { + if (BaresipService.speakerPhone || Config.variable("speaker_phone") == "yes") { + BaresipService.speakerPhone = true @Suppress("DEPRECATION") connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER) + BaresipService.postServiceEvent( + ServiceEvent("speaker update,true", + arrayListOf(uap, 0L), + System.nanoTime() + ) + ) } connection.setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED) @@ -218,15 +240,25 @@ class ConnectionService : ConnectionService() { ) } val isSpeaker = it.route == CallAudioState.ROUTE_SPEAKER - if (BaresipService.speakerPhone != isSpeaker) { - BaresipService.speakerPhone = isSpeaker - BaresipService.postServiceEvent( - ServiceEvent("speaker update,${isSpeaker}", - arrayListOf(uap, callp), - System.nanoTime() - ) - ) + val call = Call.ofCallp(callp) + val status = call?.status?.value ?: "idle" + val hasPendingOrActiveConnection = connections.isNotEmpty() || pendingOutgoingConnection != null + if (isSpeaker != BaresipService.speakerPhone && (status != "connected" || hasPendingOrActiveConnection)) { + if (status != "connected") { + 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() + ) + ) } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 116a8d93..08bfddc6 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -212,19 +212,20 @@ private fun MainScreen( val configuration = LocalConfiguration.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 showKeyboard by viewModel.showKeyboard.collectAsState() val hideKeyboard by viewModel.hideKeyboard.collectAsState() LaunchedEffect(showKeyboard) { - if (viewModel.showKeyboard.value > 0) + if (showKeyboard > 0) keyboardController?.show() } LaunchedEffect(hideKeyboard) { - if (viewModel.hideKeyboard.value > 0) + if (hideKeyboard > 0) keyboardController?.hide() } @@ -234,6 +235,7 @@ private fun MainScreen( Lifecycle.Event.ON_RESUME -> { Log.d(TAG, "Resumed to MainScreen") BaresipService.isMainVisible = true + viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone) viewModel.updateCalls(Call.calls().toList()) (Call.call("incoming") ?: Call.calls().lastOrNull())?.let { spinToAor(viewModel, it.ua.account.aor) @@ -524,8 +526,6 @@ private fun TopAppBar( val ctx = LocalContext.current val currentMicIcon by viewModel.micIcon.collectAsState() - val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val recOffImage = Icons.Filled.VoiceOverOff val recOnImage = Icons.Filled.RecordVoiceOver var isRecOn by remember { mutableStateOf(BaresipService.isRecOn) } @@ -648,7 +648,6 @@ private fun TopAppBar( .clip(CircleShape) .combinedClickable( onClick = { - val isCurrentlyOn = isSpeakerOn val aor = viewModel.selectedAor.value val ua = uas.value.find { it.account.aor == aor } val call = ua?.currentCall() @@ -657,13 +656,14 @@ private fun TopAppBar( if (connection != null) { @Suppress("DEPRECATION") connection.setAudioRoute( - if (isCurrentlyOn) + if (isSpeakerOn) android.telecom.CallAudioState.ROUTE_EARPIECE else android.telecom.CallAudioState.ROUTE_SPEAKER ) } else { - Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am) + BaresipService.speakerPhone = !isSpeakerOn + viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone) } }, onLongClick = { @@ -2291,14 +2291,32 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params return } + val ev = event.split(",") 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) if (ua == null) { handleNextEvent("handleServiceEvent '$event' did not find ua $uap") return } - val ev = event.split(",") Log.d(TAG, "Handling service event '${ev[0]}' for $uap") val acc = ua.account val aor = ua.account.aor @@ -2480,16 +2498,6 @@ 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 update" -> { - viewModel.updateSpeakerPhoneStatus(ev[1].toBoolean()) - } else -> Log.e(TAG, "Unknown event '${ev[0]}'") }