From 8148354901c0367ccdd662815f83b6b8d1014c08 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 13 Jun 2025 11:25:28 +0200 Subject: [PATCH] Added enable/disable speaker to active call notification --- CHANGELOG.md | 1 + .../NotificationBroadcastReceiver.kt | 66 ++++++++++------ .../notifications/NotificationsManager.kt | 75 +++++++++++++++---- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 110 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34bb72c3a..6f8618f16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Group changes to describe their impact on the project, as follows: ## [6.1.0] - Unreleased ### Added +- Added toggle speaker action in active call notification - Added a vu meter for recording & playback volumes (disabled by default, must be enabled in CorePreferences) - Added a setting for user to choose whether to sort contacts by first name or last name - Added a setting to hide contacts that have neither a SIP address nor a phone number diff --git a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt index ef9ff0566..7b98ba838 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt @@ -26,8 +26,10 @@ import android.content.Context import android.content.Intent import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.Address +import org.linphone.core.AudioDevice import org.linphone.core.ConferenceParams import org.linphone.core.tools.Log +import org.linphone.utils.AudioUtils class NotificationBroadcastReceiver : BroadcastReceiver() { companion object { @@ -36,47 +38,69 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val notificationId = intent.getIntExtra(NotificationsManager.INTENT_NOTIF_ID, 0) - Log.i( - "$TAG Got notification broadcast for ID [$notificationId]" - ) + val action = intent.action + Log.i("$TAG Got notification broadcast for ID [$notificationId] with action [$action]") // Wait for coreContext to be ready to handle intent while (!coreContext.isReady()) { Thread.sleep(50) } - if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION) { - handleCallIntent(intent, notificationId) - } else if (intent.action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION) { - handleChatIntent(context, intent, notificationId) + if ( + action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION || + action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION || + action == NotificationsManager.INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION + ) { + handleCallIntent(intent, notificationId, action) + } else if ( + action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION || + action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION + ) { + handleChatIntent(context, intent, notificationId, action) } } - private fun handleCallIntent(intent: Intent, notificationId: Int) { - val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_ADDRESS) - if (remoteSipAddress == null) { + private fun handleCallIntent(intent: Intent, notificationId: Int, action: String) { + val remoteSipUri = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_SIP_URI) + if (remoteSipUri == null) { Log.e("$TAG Remote SIP address is null for call notification ID [$notificationId]") return } coreContext.postOnCoreThread { core -> val call = core.calls.find { - it.remoteAddress.asStringUriOnly() == remoteSipAddress + it.remoteAddress.asStringUriOnly() == remoteSipUri } if (call == null) { - Log.e("$TAG Couldn't find call from remote address [$remoteSipAddress]") + Log.e("$TAG Couldn't find call from remote address [$remoteSipUri]") } else { - if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION) { - coreContext.answerCall(call) - } else { - coreContext.terminateCall(call) + when (action) { + NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION -> { + Log.i("$TAG Answering call with remote address [$remoteSipUri]") + coreContext.answerCall(call) + } + NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION -> { + Log.i("$TAG Declining/terminating call with remote address [$remoteSipUri]") + coreContext.terminateCall(call) + } + NotificationsManager.INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION -> { + val audioDevice = call.outputAudioDevice + val isUsingSpeaker = audioDevice?.type == AudioDevice.Type.Speaker + if (isUsingSpeaker) { + Log.i("$TAG Routing audio to earpiece for call [$remoteSipUri]") + AudioUtils.routeAudioToEarpiece(call) + } else { + Log.i("$TAG Routing audio to speaker for call [$remoteSipUri]") + AudioUtils.routeAudioToSpeaker(call) + } + } } } } } - private fun handleChatIntent(context: Context, intent: Intent, notificationId: Int) { - val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_ADDRESS) + private fun handleChatIntent(context: Context, intent: Intent, notificationId: Int, action: String) { + val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_SIP_URI) if (remoteSipAddress == null) { Log.e("$TAG Remote SIP address is null for notification ID [$notificationId]") return @@ -88,7 +112,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { } val reply = getMessageText(intent)?.toString() - if (intent.action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { + if (action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { if (reply == null) { Log.e("$TAG Couldn't get reply text") return @@ -128,13 +152,13 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { return@postOnCoreThread } - if (intent.action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { + if (action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { val msg = room.createMessageFromUtf8(reply) msg.userData = notificationId msg.addListener(coreContext.notificationsManager.chatMessageListener) msg.send() Log.i("$TAG Reply sent for notif id [$notificationId]") - } else if (intent.action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION) { + } else if (action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION) { Log.i("$TAG Marking chat room from notification id [$notificationId] as read") room.markAsRead() if (!coreContext.notificationsManager.dismissChatNotification(room)) { diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 1e11eab28..1f5bea5f2 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -54,6 +54,7 @@ import org.linphone.contacts.ContactsManager.ContactsListener import org.linphone.contacts.getAvatarBitmap import org.linphone.contacts.getPerson import org.linphone.core.Address +import org.linphone.core.AudioDevice import org.linphone.core.Call import org.linphone.core.ChatMessage import org.linphone.core.ChatMessageListener @@ -86,13 +87,14 @@ class NotificationsManager const val INTENT_HANGUP_CALL_NOTIF_ACTION = "org.linphone.HANGUP_CALL_ACTION" const val INTENT_ANSWER_CALL_NOTIF_ACTION = "org.linphone.ANSWER_CALL_ACTION" + const val INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION = "org.linphone.TOGGLE_SPEAKER_CALL_ACTION" const val INTENT_REPLY_MESSAGE_NOTIF_ACTION = "org.linphone.REPLY_ACTION" const val INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION = "org.linphone.MARK_AS_READ_ACTION" const val INTENT_NOTIF_ID = "NOTIFICATION_ID" const val KEY_TEXT_REPLY = "key_text_reply" const val INTENT_LOCAL_IDENTITY = "LOCAL_IDENTITY" - const val INTENT_REMOTE_ADDRESS = "REMOTE_ADDRESS" + const val INTENT_REMOTE_SIP_URI = "REMOTE_ADDRESS" const val CHAT_TAG = "Chat" private const val MISSED_CALL_TAG = "Missed call" @@ -151,7 +153,12 @@ class NotificationsManager Log.i( "$TAG Found call [${addressMatch.asStringUriOnly()}] with contact in notifications, updating it" ) - updateCallNotification(notifiable, addressMatch, friend) + val call = coreContext.core.getCallByRemoteAddress2(addressMatch) + if (call == null) { + Log.e("$TAG Failed to get Call from Core using remote address [${addressMatch.asStringUriOnly()}]") + return + } + updateCallNotification(notifiable, call, friend) } } @@ -276,6 +283,18 @@ class NotificationsManager } } + @WorkerThread + override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) { + if (core.callsNb == 0) return + + val call = core.currentCall ?: core.calls.firstOrNull() + if (call != null) { + Log.i("$TAG Audio device changed, updating call [${call.remoteAddress.asStringUriOnly()}] notification") + val notifiable = getNotifiableForCall(call) + updateCallNotification(notifiable, call, null) + } + } + @WorkerThread override fun onMessagesReceived( core: Core, @@ -1292,22 +1311,33 @@ class NotificationsManager setFullScreenIntent(pendingIntent, true) } + if (!isIncoming) { + val toggleSpeakerIntent = getCallToggleSpeakerPendingIntent(notifiable) + + val audioDevice = call.outputAudioDevice + val isUsingSpeaker = audioDevice?.type == AudioDevice.Type.Speaker + + val toggleSpeakerAction = if (isUsingSpeaker) { + Log.i("$TAG Call is using speaker, adding action to disable it") + val text = AppUtils.getString(R.string.notification_disable_speaker_for_call) + NotificationCompat.Action.Builder(R.drawable.speaker_slash, text, toggleSpeakerIntent).build() + } else { + Log.i("$TAG Call is not using speaker, adding action to enable it") + val text = AppUtils.getString(R.string.notification_enable_speaker_for_call) + NotificationCompat.Action.Builder(R.drawable.speaker_high, text, toggleSpeakerIntent).build() + } + builder.addAction(toggleSpeakerAction) + } + return builder.build() } @WorkerThread private fun updateCallNotification( notifiable: Notifiable, - remoteAddress: Address, - friend: Friend + call: Call, + friend: Friend? ) { - val call = coreContext.core.getCallByRemoteAddress2(remoteAddress) - if (call == null) { - Log.w( - "$TAG Failed to find call with remote SIP URI [${remoteAddress.asStringUriOnly()}]" - ) - return - } val isIncoming = LinphoneUtils.isCallIncoming(call.state) val notification = if (isIncoming) { @@ -1469,7 +1499,7 @@ class NotificationsManager val hangupIntent = Intent(context, NotificationBroadcastReceiver::class.java) hangupIntent.action = INTENT_HANGUP_CALL_NOTIF_ACTION hangupIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) - hangupIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + hangupIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) return PendingIntent.getBroadcast( context, @@ -1484,7 +1514,7 @@ class NotificationsManager val answerIntent = Intent(context, NotificationBroadcastReceiver::class.java) answerIntent.action = INTENT_ANSWER_CALL_NOTIF_ACTION answerIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) - answerIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + answerIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) return PendingIntent.getBroadcast( context, @@ -1494,6 +1524,21 @@ class NotificationsManager ) } + @AnyThread + fun getCallToggleSpeakerPendingIntent(notifiable: Notifiable): PendingIntent { + val answerIntent = Intent(context, NotificationBroadcastReceiver::class.java) + answerIntent.action = INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION + answerIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) + answerIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) + + return PendingIntent.getBroadcast( + context, + 4, + answerIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + @WorkerThread private fun displayReplyMessageNotification(message: ChatMessage, notifiable: Notifiable) { Log.i( @@ -1535,7 +1580,7 @@ class NotificationsManager replyIntent.action = INTENT_REPLY_MESSAGE_NOTIF_ACTION replyIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) replyIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) - replyIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + replyIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) // PendingIntents attached to actions with remote inputs must be mutable val replyPendingIntent = PendingIntent.getBroadcast( @@ -1562,7 +1607,7 @@ class NotificationsManager markAsReadIntent.action = INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION markAsReadIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) markAsReadIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) - markAsReadIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + markAsReadIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) return PendingIntent.getBroadcast( context, diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bb5a546ef..9fef4ace3 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -69,6 +69,8 @@ %s fichiers en cours de réception Cliquez pour ouvrir + Activer haut-parleur + Désactiver haut-parleur Bienvenue diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42be308f7..cccb21e02 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -110,6 +110,8 @@ %s, %s Click to open + Turn on speaker + Turn off speaker Welcome