Added enable/disable speaker to active call notification

This commit is contained in:
Sylvain Berfini 2025-06-13 11:25:28 +02:00
parent ae39d79420
commit 8148354901
5 changed files with 110 additions and 36 deletions

View file

@ -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

View file

@ -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)) {

View file

@ -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,

View file

@ -69,6 +69,8 @@
<item quantity="other">%s fichiers en cours de réception</item>
</plurals>
<string name="notification_keep_app_alive_message">Cliquez pour ouvrir</string>
<string name="notification_enable_speaker_for_call">Activer haut-parleur</string>
<string name="notification_disable_speaker_for_call">Désactiver haut-parleur</string>
<!-- First screens user see when app is installed and started -->
<string name="welcome_page_title">Bienvenue</string>

View file

@ -110,6 +110,8 @@
</plurals>
<string name="notification_file_transfer_upload_download_message" translatable="false">%s, %s</string>
<string name="notification_keep_app_alive_message">Click to open</string>
<string name="notification_enable_speaker_for_call">Turn on speaker</string>
<string name="notification_disable_speaker_for_call">Turn off speaker</string>
<!-- First screens user see when app is installed and started -->
<string name="welcome_page_title">Welcome</string>