Reworked notifications channel, using incoming call one for playing ringtone instead of SDK

This commit is contained in:
Sylvain Berfini 2024-04-02 15:49:23 +02:00
parent 44457665ef
commit 0707e60c26
7 changed files with 194 additions and 145 deletions

View file

@ -27,6 +27,7 @@ rls_uri=sips:rls@sip.linphone.org
[sound]
#remove this property for any application that is not Linphone public version itself
ec_calibrator_cool_tones=1
disable_ringing=1
[audio]
android_disable_audio_focus_requests=1

View file

@ -262,6 +262,8 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.registerAudioDeviceCallback(audioDeviceCallback, coreThread)
corePreferences.linphoneConfigurationVersion = "6.0"
Log.i("$TAG Report Core created and started")
}

View file

@ -126,6 +126,13 @@ class CorePreferences @UiThread constructor(private val context: Context) {
config.setInt("app", "dark_mode", value)
}
@get:WorkerThread @set:WorkerThread
var linphoneConfigurationVersion: String
get() = config.getString("app", "linphonerc_version", "5.2")!!
set(value) {
config.setString("app", "linphonerc_version", value)
}
@get:WorkerThread
val darkModeAllowed: Boolean
get() = config.getBool("ui", "dark_mode_allowed", true)

View file

@ -29,6 +29,9 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.RingtoneManager
import android.net.Uri
import android.os.Bundle
import androidx.annotation.AnyThread
@ -88,6 +91,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
private const val MISSED_CALL_TAG = "Missed call"
const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP"
private const val INCOMING_CALL_ID = 1
private const val MISSED_CALL_ID = 10
}
@ -321,12 +325,6 @@ class NotificationsManager @MainThread constructor(private val context: Context)
private var currentlyDisplayedChatRoomId: String = ""
init {
createServiceChannel()
createIncomingCallNotificationChannel()
createMissedCallNotificationChannel()
createActiveCallNotificationChannel()
createMessageChannel()
for (notification in notificationManager.activeNotifications) {
if (notification.tag.isNullOrEmpty()) {
Log.w(
@ -365,8 +363,10 @@ class NotificationsManager @MainThread constructor(private val context: Context)
if (core.callsNb == 0) {
Log.w("$TAG No call anymore, stopping service")
stopCallForeground()
} else {
Log.i("$TAG At least a call is still running")
} else if (currentForegroundServiceNotificationId == -1) {
Log.i(
"$TAG At least a call is still running and no foreground service notification was found"
)
val call = core.currentCall ?: core.calls.first()
startCallForeground(call)
}
@ -379,9 +379,33 @@ class NotificationsManager @MainThread constructor(private val context: Context)
coreService = null
}
@MainThread
private fun createChannels(clearPreviousChannels: Boolean) {
if (clearPreviousChannels) {
Log.w("$TAG We were asked to remove all existing notification channels")
for (channel in notificationManager.notificationChannels) {
Log.i("$TAG Deleting notification channel ID [${channel.id}]")
notificationManager.deleteNotificationChannel(channel.id)
}
}
createServiceChannel()
createIncomingCallNotificationChannel()
createMissedCallNotificationChannel()
createActiveCallNotificationChannel()
createMessageChannel()
}
@WorkerThread
fun onCoreStarted(core: Core) {
Log.i("$TAG Core has been started")
val rcVersion = corePreferences.linphoneConfigurationVersion
val clearPreviousChannels = rcVersion == "5.2"
coreContext.postOnMainThread {
createChannels(clearPreviousChannels)
}
core.addListener(coreListener)
}
@ -394,6 +418,22 @@ class NotificationsManager @MainThread constructor(private val context: Context)
@WorkerThread
private fun showCallNotification(call: Call, isIncoming: Boolean) {
val notifiable = getNotifiableForCall(call)
if (!isIncoming && call.dir == Call.Dir.Incoming) {
if (currentForegroundServiceNotificationId == INCOMING_CALL_ID) {
// This is an accepted incoming call, remove notification before adding it again to change channel
Log.i(
"$TAG Incoming call with notification ID [${notifiable.notificationId}] was accepted, cancelling notification before adding it again to the right channel"
)
if (coreService != null) {
Log.i(
"$TAG Service found, stopping it as foreground before cancelling notification"
)
coreService?.stopForeground(STOP_FOREGROUND_REMOVE)
}
cancelNotification(INCOMING_CALL_ID)
currentForegroundServiceNotificationId = -1
}
}
val callNotificationIntent = Intent(context, CallActivity::class.java)
callNotificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@ -417,10 +457,16 @@ class NotificationsManager @MainThread constructor(private val context: Context)
pendingIntent,
isIncoming
)
notify(notifiable.notificationId, notification)
if (notifiable.notificationId == currentForegroundServiceNotificationId) {
startCallForeground(call)
if (isIncoming) {
notify(INCOMING_CALL_ID, notification)
if (currentForegroundServiceNotificationId == -1) {
startIncomingCallForeground(notification)
}
} else {
notify(notifiable.notificationId, notification)
if (currentForegroundServiceNotificationId == -1) {
startCallForeground(call)
}
}
}
@ -465,6 +511,26 @@ class NotificationsManager @MainThread constructor(private val context: Context)
notify(MISSED_CALL_ID, notification, MISSED_CALL_TAG)
}
@WorkerThread
private fun startIncomingCallForeground(notification: Notification) {
Log.i("$TAG Trying to start foreground Service using incoming call notification")
val service = coreService
if (service != null) {
Log.i(
"$TAG Service found, starting it as foreground using notification ID [$INCOMING_CALL_ID] with type PHONE_CALL"
)
Compatibility.startServiceForeground(
service,
INCOMING_CALL_ID,
notification,
Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL
)
currentForegroundServiceNotificationId = INCOMING_CALL_ID
} else {
Log.w("$TAG Core Foreground Service hasn't started yet...")
}
}
@WorkerThread
private fun startCallForeground(call: Call) {
Log.i("$TAG Trying to start/update foreground Service using call notification")
@ -863,19 +929,15 @@ class NotificationsManager @MainThread constructor(private val context: Context)
channel
).apply {
try {
style.setIsVideo(isVideo)
style.setAnswerButtonColorHint(
context.getColor(R.color.success_500)
)
style.setDeclineButtonColorHint(
context.getColor(R.color.danger_500)
)
style.setIsVideo(false)
setStyle(style)
} catch (iae: IllegalArgumentException) {
Log.e(
"$TAG Can't use notification call style: $iae"
)
}
setColorized(true)
setOnlyAlertOnce(true)
setSmallIcon(smallIcon)
setCategory(NotificationCompat.CATEGORY_CALL)
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
@ -1017,7 +1079,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
return PendingIntent.getBroadcast(
context,
notifiable.notificationId,
3,
hangupIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
@ -1032,7 +1094,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
return PendingIntent.getBroadcast(
context,
notifiable.notificationId,
2,
answerIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
@ -1146,13 +1208,16 @@ class NotificationsManager @MainThread constructor(private val context: Context)
val id = context.getString(R.string.notification_channel_incoming_call_id)
val name = context.getString(R.string.notification_channel_incoming_call_name)
val ringtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(AudioManager.STREAM_RING)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build()
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH).apply {
description = name
lightColor = context.getColor(R.color.main1_500)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
enableVibration(true)
enableLights(true)
setShowBadge(false)
setSound(ringtone, audioAttributes)
}
notificationManager.createNotificationChannel(channel)
}
@ -1164,11 +1229,8 @@ class NotificationsManager @MainThread constructor(private val context: Context)
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH).apply {
description = name
lightColor = context.getColor(R.color.main1_500)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
enableVibration(true)
enableLights(true)
setShowBadge(false)
}
notificationManager.createNotificationChannel(channel)
}
@ -1181,9 +1243,6 @@ class NotificationsManager @MainThread constructor(private val context: Context)
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT).apply {
description = name
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
enableVibration(false)
enableLights(false)
setShowBadge(false)
}
notificationManager.createNotificationChannel(channel)
}
@ -1195,11 +1254,8 @@ class NotificationsManager @MainThread constructor(private val context: Context)
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH).apply {
description = name
lightColor = context.getColor(R.color.main1_500)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
enableLights(true)
enableVibration(true)
setShowBadge(true)
}
notificationManager.createNotificationChannel(channel)
}
@ -1211,9 +1267,6 @@ class NotificationsManager @MainThread constructor(private val context: Context)
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW).apply {
description = name
enableVibration(false)
enableLights(false)
setShowBadge(false)
}
notificationManager.createNotificationChannel(channel)
}

View file

@ -34,7 +34,6 @@ import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.AudioDevice
import org.linphone.core.Call
import org.linphone.core.CallListenerStub
import org.linphone.core.MediaDirection
import org.linphone.core.tools.Log
import org.linphone.utils.AudioUtils
@ -55,15 +54,9 @@ class TelecomCallControlCallback(
Log.i("$TAG Call [${call.remoteAddress.asStringUriOnly()}] state changed [$state]")
if (state == Call.State.Connected) {
if (call.dir == Call.Dir.Incoming) {
val videoEnabled = call.currentParams.isVideoEnabled && call.currentParams.videoDirection != MediaDirection.Inactive
scope.launch {
val type = if (videoEnabled) {
CallAttributesCompat.CALL_TYPE_VIDEO_CALL
} else {
CallAttributesCompat.CALL_TYPE_AUDIO_CALL
}
Log.i("$TAG Answering call with type [$type]")
callControl.answer(type)
Log.i("$TAG Answering call")
callControl.answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)
}
} else {
scope.launch {
@ -74,7 +67,7 @@ class TelecomCallControlCallback(
} else if (state == Call.State.End) {
scope.launch {
Log.i("$TAG Disconnecting call")
callControl.disconnect(DisconnectCause(DisconnectCause.REMOTE))
callControl.disconnect(DisconnectCause(DisconnectCause.LOCAL))
}
} else if (state == Call.State.Pausing) {
scope.launch {

View file

@ -33,7 +33,6 @@ import org.linphone.core.AudioDevice
import org.linphone.core.Call
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.MediaDirection
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
@ -51,98 +50,7 @@ class TelecomManager @WorkerThread constructor(context: Context) {
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onCallCreated(core: Core, call: Call) {
Log.i("$TAG Call created: $call")
val address = call.remoteAddress
val friend = coreContext.contactsManager.findContactByAddress(address)
val displayName = friend?.name ?: LinphoneUtils.getDisplayName(address)
val uri = Uri.parse(address.asStringUriOnly())
val direction = if (call.dir == Call.Dir.Outgoing) {
CallAttributesCompat.DIRECTION_OUTGOING
} else {
CallAttributesCompat.DIRECTION_INCOMING
}
val params = if (call.dir == Call.Dir.Outgoing) {
call.params
} else {
call.remoteParams
}
val type = if (params?.isVideoEnabled == true && params.videoDirection != MediaDirection.Inactive) {
CallAttributesCompat.CALL_TYPE_VIDEO_CALL
} else {
CallAttributesCompat.CALL_TYPE_AUDIO_CALL
}
val capabilities = CallAttributesCompat.SUPPORTS_SET_INACTIVE or CallAttributesCompat.SUPPORTS_TRANSFER
val callAttributes = CallAttributesCompat(
displayName,
uri,
direction,
type,
capabilities
)
Log.i("$TAG Adding call to Telecom's CallsManager with attributes [$callAttributes]")
scope.launch {
try {
callsManager.addCall(
callAttributes,
{ callType -> // onAnswer
Log.i("$TAG We're asked to answer the call with type [$callType]")
coreContext.postOnCoreThread {
if (LinphoneUtils.isCallIncoming(call.state)) {
Log.i("$TAG Answering call")
coreContext.answerCall(call) // TODO FIXME: use call type
}
}
},
{ disconnectCause -> // onDisconnect
Log.i(
"$TAG We're asked to terminate the call with reason [$disconnectCause]"
)
coreContext.postOnCoreThread {
Log.i(
"$TAG Terminating call [${call.remoteAddress.asStringUriOnly()}]"
)
call.terminate() // TODO FIXME: use cause
}
},
{ // onSetActive
Log.i("$TAG We're asked to resume the call")
coreContext.postOnCoreThread {
Log.i("$TAG Resuming call")
call.resume()
}
},
{ // onSetInactive
Log.i("$TAG We're asked to pause the call")
coreContext.postOnCoreThread {
Log.i("$TAG Pausing call")
call.pause()
}
}
) {
val callbacks = TelecomCallControlCallback(call, this, scope)
coreContext.postOnCoreThread {
val callId = call.callLog.callId.orEmpty()
if (callId.isNotEmpty()) {
Log.i("$TAG Storing our callbacks for call ID [$callId]")
map[callId] = callbacks
}
}
// We must first call setCallback on callControlScope before using it
callbacks.onCallControlCallbackSet()
}
} catch (e: Exception) {
Log.e("$TAG Failed to add call to Telecom's CallsManager!")
}
}
onCallCreated(call)
}
}
@ -157,6 +65,91 @@ class TelecomManager @WorkerThread constructor(context: Context) {
)
}
@WorkerThread
fun onCallCreated(call: Call) {
Log.i("$TAG Call created: $call")
val address = call.remoteAddress
val friend = coreContext.contactsManager.findContactByAddress(address)
val displayName = friend?.name ?: LinphoneUtils.getDisplayName(address)
val uri = Uri.parse(address.asStringUriOnly())
val direction = if (call.dir == Call.Dir.Outgoing) {
CallAttributesCompat.DIRECTION_OUTGOING
} else {
CallAttributesCompat.DIRECTION_INCOMING
}
val capabilities = CallAttributesCompat.SUPPORTS_SET_INACTIVE or CallAttributesCompat.SUPPORTS_TRANSFER
val callAttributes = CallAttributesCompat(
displayName,
uri,
direction,
CallAttributesCompat.CALL_TYPE_AUDIO_CALL,
capabilities
)
Log.i("$TAG Adding call to Telecom's CallsManager with attributes [$callAttributes]")
scope.launch {
try {
callsManager.addCall(
callAttributes,
{ callType -> // onAnswer
Log.i("$TAG We're asked to answer the call with type [$callType]")
coreContext.postOnCoreThread {
if (LinphoneUtils.isCallIncoming(call.state)) {
Log.i("$TAG Answering call")
coreContext.answerCall(call)
}
}
},
{ disconnectCause -> // onDisconnect
Log.i(
"$TAG We're asked to terminate the call with reason [$disconnectCause]"
)
coreContext.postOnCoreThread {
Log.i(
"$TAG Terminating call [${call.remoteAddress.asStringUriOnly()}]"
)
call.terminate() // TODO FIXME: use cause
}
},
{ // onSetActive
Log.i("$TAG We're asked to resume the call")
coreContext.postOnCoreThread {
Log.i("$TAG Resuming call")
call.resume()
}
},
{ // onSetInactive
Log.i("$TAG We're asked to pause the call")
coreContext.postOnCoreThread {
Log.i("$TAG Pausing call")
call.pause()
}
}
) {
val callbacks = TelecomCallControlCallback(call, this, scope)
coreContext.postOnCoreThread {
val callId = call.callLog.callId.orEmpty()
if (callId.isNotEmpty()) {
Log.i("$TAG Storing our callbacks for call ID [$callId]")
map[callId] = callbacks
}
}
// We must first call setCallback on callControlScope before using it
callbacks.onCallControlCallbackSet()
}
} catch (e: Exception) {
Log.e("$TAG Failed to add call to Telecom's CallsManager!")
}
}
}
@WorkerThread
fun onCoreStarted(core: Core) {
Log.i("$TAG Core has been started")

View file

@ -13,11 +13,11 @@
<string name="assistant_web_platform_link" translatable="false">subscribe.linphone.org</string>
<string name="assistant_linphone_contact_us_link" translatable="false">linphone.org/contact</string>
<string name="notification_channel_call_id" translatable="false">linphone_notification_call_id</string>
<string name="notification_channel_incoming_call_id" translatable="false">linphone_notification_incoming_call_id</string>
<string name="notification_channel_missed_call_id" translatable="false">linphone_notification_missed_call_id</string>
<string name="notification_channel_service_id" translatable="false">linphone_notification_service_id</string>
<string name="notification_channel_chat_id" translatable="false">linphone_notification_chat_id</string>
<string name="notification_channel_call_id" translatable="false">linphone_6.0_notification_call_id</string>
<string name="notification_channel_incoming_call_id" translatable="false">linphone_6.0_notification_incoming_call_id</string>
<string name="notification_channel_missed_call_id" translatable="false">linphone_6.0_notification_missed_call_id</string>
<string name="notification_channel_service_id" translatable="false">linphone_6.0_notification_service_id</string>
<string name="notification_channel_chat_id" translatable="false">linphone_6.0_notification_chat_id</string>
<string name="emoji_love" translatable="false">❤️</string>
<string name="emoji_thumbs_up" translatable="false">👍</string>