From 0707e60c26b23440d4d4378634c72dcdc38ba8be Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 2 Apr 2024 15:49:23 +0200 Subject: [PATCH] Reworked notifications channel, using incoming call one for playing ringtone instead of SDK --- app/src/main/assets/linphonerc_factory | 1 + .../java/org/linphone/core/CoreContext.kt | 2 + .../java/org/linphone/core/CorePreferences.kt | 7 + .../notifications/NotificationsManager.kt | 127 +++++++++---- .../telecom/TelecomCallControlCallback.kt | 13 +- .../org/linphone/telecom/TelecomManager.kt | 179 +++++++++--------- app/src/main/res/values/strings.xml | 10 +- 7 files changed, 194 insertions(+), 145 deletions(-) diff --git a/app/src/main/assets/linphonerc_factory b/app/src/main/assets/linphonerc_factory index 58a9c38e2..fc2954596 100644 --- a/app/src/main/assets/linphonerc_factory +++ b/app/src/main/assets/linphonerc_factory @@ -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 diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 74be178cd..67b2827d5 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -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") } diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 43c6ae520..958fe90de 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -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) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 988120b86..8adb34914 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -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) } diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index f209c11de..1f1539793 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -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 { diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt index 2f790afd0..554d1377e 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomManager.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt @@ -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") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 19f2a18df..d96a62b76 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,11 +13,11 @@ subscribe.linphone.org linphone.org/contact - linphone_notification_call_id - linphone_notification_incoming_call_id - linphone_notification_missed_call_id - linphone_notification_service_id - linphone_notification_chat_id + linphone_6.0_notification_call_id + linphone_6.0_notification_incoming_call_id + linphone_6.0_notification_missed_call_id + linphone_6.0_notification_service_id + linphone_6.0_notification_chat_id ❤️ 👍