From 37200ecf8fa3420a4ddb62555ec3ffbdb86e5952 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 23 Feb 2026 09:56:29 +0100 Subject: [PATCH] Show error notification if in-call foreground service doesn't starts successfully to let user know there is an issue and clicking on the notification will fix it --- CHANGELOG.md | 1 + .../notifications/NotificationsManager.kt | 182 +++++++++++++----- .../java/org/linphone/ui/call/CallActivity.kt | 4 + .../java/org/linphone/utils/LinphoneUtils.kt | 8 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 153 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ec704c7d..9493f4b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Group changes to describe their impact on the project, as follows: - Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app - Improved navigation within app when using a keyboard - Now loading media/documents contents in conversation by chunks (instead of all of them at once) +- If in-call foreground service doesn't start, show an error notification and clicking on it will fix the issue (by bringing Linphone in foreground and re-starting the foreground service) - Simplified audio device name in settings - Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) - Removed menu to access account profile, button is now directly available from drawer menu diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 62e01e5b4..6bde59727 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -106,6 +106,7 @@ class NotificationsManager const val CHAT_TAG = "Chat" private const val ACCOUNT_ERROR_TAG = "Account Error" + private const val IN_CALL_ERROR_TAG = "Call Error" private const val MISSED_CALL_TAG = "Missed call" private const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" @@ -113,6 +114,7 @@ class NotificationsManager private const val DUMMY_NOTIF_ID = 3 private const val KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID = 5 private const val ACCOUNT_REGISTRATION_ERROR_ID = 7 + private const val IN_CALL_FOREGROUND_SERVICE_ERROR_ID = 8 private const val MISSED_CALL_ID = 10 } @@ -226,13 +228,14 @@ class NotificationsManager Log.i( "$TAG Updating incoming call notification to active call for [${call.remoteAddress.asStringUriOnly()}]" ) + currentlyRingingCallRemoteAddress = null showCallNotification(call, false) } Call.State.StreamsRunning -> { val notifiable = getNotifiableForCall(call) if (notifiable.notificationId == currentInCallServiceNotificationId) { Log.i( - "$TAG Update foreground service type in case video was enabled/disabled since last time" + "$TAG Update foreground Service type in case video was enabled/disabled since last time" ) startInCallForegroundService(call) } @@ -282,12 +285,17 @@ class NotificationsManager override fun onLastCallEnded(core: Core) { Log.i("$TAG Last call ended") if (inCallServiceForegroundNotificationPublished) { - Log.i("$TAG Stopping foreground service") + Log.i("$TAG Stopping foreground Service") stopInCallForegroundService() } else { Log.i("$TAG In-Call service was never started as foreground, waiting for it to be started to stop it") waitForInCallServiceForegroundToStopIt = true } + + if (notificationsMap.containsKey(IN_CALL_FOREGROUND_SERVICE_ERROR_ID)) { + Log.i("$TAG Removing in-call foreground Service error notification") + cancelNotification(IN_CALL_FOREGROUND_SERVICE_ERROR_ID, IN_CALL_ERROR_TAG) + } } @WorkerThread @@ -673,6 +681,26 @@ class NotificationsManager coreContext.contactsManager.removeListener(contactsListener) } + @WorkerThread + fun showInCallForegroundServiceNotificationIfNeeded() { + if (currentInCallServiceNotificationId == -1) { + Log.w("$TAG No current in-call foreground Service notification found, try to create it now") + val call = coreContext.core.currentCall ?: coreContext.core.calls.find { + LinphoneUtils.isCallActive(it.state) + } ?: coreContext.core.calls.find { + LinphoneUtils.isCallPaused(it.state) + } + if (call != null) { + Log.i("$TAG Using call [${call.remoteAddress.asStringUriOnly()}] for foreground Service notification") + showCallNotification(call, LinphoneUtils.isCallIncoming(call.state)) + } else { + Log.w("$TAG No active call found for foreground Service notification, aborting") + } + } else { + Log.i("$TAG There is already a foreground Service notification for a call, nothing to do") + } + } + @WorkerThread fun removeIncomingCallNotificationIfAny(call: Call) { val notifiable = getNotifiableForCall(call) @@ -730,14 +758,14 @@ class NotificationsManager if (isIncoming) { currentlyRingingCallRemoteAddress = call.remoteAddress if (currentInCallServiceNotificationId == -1) { - Log.i("$TAG No current in-call foreground service notification found, using this one") + Log.i("$TAG No current in-call foreground Service notification found, using this one") showIncomingCallForegroundServiceNotification(notifiable.notificationId, notification) } else { notify(notifiable.notificationId, notification) } } else { if (currentInCallServiceNotificationId == -1) { - Log.i("$TAG No current in-call foreground service notification found, using this one") + Log.i("$TAG No current in-call foreground Service notification found, using this one") showInCallForegroundServiceNotification(call, notifiable, notification) } else { notify(notifiable.notificationId, notification) @@ -807,20 +835,21 @@ class NotificationsManager notification, Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL ) - if (!success) { - Log.e("$TAG Failed to start incoming call foreground service!") - } - notificationsMap[notificationId] = notification - currentInCallServiceNotificationId = notificationId - inCallServiceForegroundNotificationPublished = true - Log.i("$TAG Incoming call notification with ID [$notificationId] has been used to start service as foreground") + if (success) { + notificationsMap[notificationId] = notification + currentInCallServiceNotificationId = notificationId + inCallServiceForegroundNotificationPublished = true + Log.i("$TAG Incoming call notification with ID [$notificationId] has been used to start service as foreground") - if (waitForInCallServiceForegroundToStopIt) { - Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") - stopInCallForegroundService() + if (waitForInCallServiceForegroundToStopIt) { + Log.i("$TAG We were waiting for foreground Service to be started to stop it, doing it") + stopInCallForegroundService() + } + } else { + Log.e("$TAG Failed to start incoming call foreground Service!") } } else { - Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") + Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground Service!") } } else { Log.w("$TAG Core Foreground Service hasn't started yet...") @@ -853,7 +882,7 @@ class NotificationsManager val channel = notificationManager.getNotificationChannel(channelId) val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE if (importance == NotificationManagerCompat.IMPORTANCE_NONE) { - Log.e("$TAG Calls channel has been disabled, can't start foreground service!") + Log.e("$TAG Calls channel has been disabled, can't start foreground Service!") stopInCallForegroundService() return } @@ -897,7 +926,7 @@ class NotificationsManager ) { mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_MICROPHONE Log.i( - "$TAG RECORD_AUDIO permission has been granted, adding FOREGROUND_SERVICE_TYPE_MICROPHONE to foreground Service types mask" + "$TAG RECORD_AUDIO permission has been granted, adding MICROPHONE to foreground Service types mask" ) } val isSendingVideo = when (call.currentParams.videoDirection) { @@ -912,7 +941,7 @@ class NotificationsManager ) { mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_CAMERA Log.i( - "$TAG CAMERA permission has been granted, adding FOREGROUND_SERVICE_TYPE_CAMERA to foreground Service types mask" + "$TAG CAMERA permission has been granted, adding CAMERA to foreground Service types mask" ) } } @@ -928,20 +957,35 @@ class NotificationsManager notification, mask ) - if (!success) { - Log.e("$TAG Failed to start call foreground service!") - } - notificationsMap[notifiable.notificationId] = notification - currentInCallServiceNotificationId = notifiable.notificationId - inCallServiceForegroundNotificationPublished = true - Log.i("$TAG Call notification with ID [${notifiable.notificationId}] has been used to start service as foreground") + if (success) { + if (notificationsMap.containsKey(IN_CALL_FOREGROUND_SERVICE_ERROR_ID)) { + Log.i("$TAG Removing previous in-call foreground Service error notification") + cancelNotification(IN_CALL_FOREGROUND_SERVICE_ERROR_ID, IN_CALL_ERROR_TAG) + } - if (waitForInCallServiceForegroundToStopIt) { - Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") - stopInCallForegroundService() + notificationsMap[notifiable.notificationId] = notification + currentInCallServiceNotificationId = notifiable.notificationId + inCallServiceForegroundNotificationPublished = true + Log.i("$TAG Call notification with ID [${notifiable.notificationId}] has been used to start service as foreground") + + if (waitForInCallServiceForegroundToStopIt) { + Log.i("$TAG We were waiting for foreground Service to be started to stop it, doing it") + stopInCallForegroundService() + } + } else { + Log.e("$TAG Failed to start call foreground Service!") + // In case of incoming call the notification ID would be in the map + // so we have to remove it as notification is no longer displayed + if (notificationsMap.containsKey(notifiable.notificationId)) { + notificationsMap.remove(notifiable.notificationId) + } + if (currentInCallServiceNotificationId == notifiable.notificationId) { + currentInCallServiceNotificationId = -1 + } + showInCallForegroundServiceErrorNotification() } } else { - Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") + Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground Service!") } } @@ -982,15 +1026,16 @@ class NotificationsManager notification, Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL ) - if (!success) { - Log.e("$TAG Failed to start dummy call foreground service!") + if (success) { + notificationsMap[DUMMY_NOTIF_ID] = notification + currentInCallServiceNotificationId = DUMMY_NOTIF_ID + inCallServiceForegroundNotificationPublished = true + Log.i("$TAG Dummy notification with ID [$DUMMY_NOTIF_ID] has been used to start service as foreground") + } else { + Log.e("$TAG Failed to start dummy call foreground Service!") } - notificationsMap[DUMMY_NOTIF_ID] = notification - currentInCallServiceNotificationId = DUMMY_NOTIF_ID - inCallServiceForegroundNotificationPublished = true - Log.i("$TAG Dummy notification with ID [$DUMMY_NOTIF_ID] has been used to start service as foreground") } else { - Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") + Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground Service!") } } else { Log.w("$TAG Core Foreground Service hasn't started yet...") @@ -1001,13 +1046,15 @@ class NotificationsManager private fun stopInCallForegroundService() { val service = inCallService if (service != null) { - Log.i( - "$TAG Stopping foreground Service (was using notification ID [$currentInCallServiceNotificationId])" - ) - service.stopForeground(STOP_FOREGROUND_REMOVE) - service.stopSelf() - inCallServiceForegroundNotificationPublished = false - waitForInCallServiceForegroundToStopIt = false + if (currentInCallServiceNotificationId != -1) { + Log.i( + "$TAG Stopping foreground Service (was using notification ID [$currentInCallServiceNotificationId])" + ) + service.stopForeground(STOP_FOREGROUND_REMOVE) + service.stopSelf() + inCallServiceForegroundNotificationPublished = false + waitForInCallServiceForegroundToStopIt = false + } } else { Log.w("$TAG Can't stop foreground Service & notif, no Service was found") } @@ -1228,6 +1275,44 @@ class NotificationsManager } } + @WorkerThread + private fun showInCallForegroundServiceErrorNotification() { + if (Compatibility.isPostNotificationsPermissionGranted(context)) { + val pendingIntent = TaskStackBuilder.create(context).run { + addNextIntentWithParentStack( + Intent(context, CallActivity::class.java).apply { + action = Intent.ACTION_MAIN // Needed as well + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + } + ) + getPendingIntent( + IN_CALL_FOREGROUND_SERVICE_ERROR_ID, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + )!! + } + + val notification = NotificationCompat.Builder( + context, + context.getString(R.string.notification_channel_account_error_id) + ) + .setContentTitle(context.getString(R.string.notification_in_call_foreground_service_error_title)) + .setContentText(context.getString(R.string.notification_in_call_foreground_service_error_message)) + .setSmallIcon(R.drawable.warning_circle) + .setAutoCancel(true) + .setOngoing(false) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setContentIntent(pendingIntent) + .build() + + val notificationId = IN_CALL_FOREGROUND_SERVICE_ERROR_ID + Log.i("$TAG Showing in-call foreground Service error notification with ID [$notificationId]") + notificationsMap[notificationId] = notification + notify(notificationId, notification, IN_CALL_ERROR_TAG) + } + } + @SuppressLint("MissingPermission") @WorkerThread private fun notify(id: Int, notification: Notification, tag: String? = null) { @@ -1573,7 +1658,12 @@ class NotificationsManager val address = call.remoteAddress.asStringUriOnly() val notifiable: Notifiable? = callNotificationsMap[address] if (notifiable != null) { - cancelNotification(notifiable.notificationId) + if (notificationsMap.containsKey(notifiable.notificationId)) { + cancelNotification(notifiable.notificationId) + } else if (notificationsMap.containsKey(IN_CALL_FOREGROUND_SERVICE_ERROR_ID)) { + Log.i("$TAG Removing previous in-call foreground Service error notification") + cancelNotification(IN_CALL_FOREGROUND_SERVICE_ERROR_ID, IN_CALL_ERROR_TAG) + } callNotificationsMap.remove(address) } else { Log.w("$TAG No notification found for call with remote address [$address]") @@ -1777,7 +1867,7 @@ class NotificationsManager val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE if (importance == NotificationManagerCompat.IMPORTANCE_NONE) { Log.e( - "$TAG Keep alive for third party accounts Service channel has been disabled, can't start foreground service!" + "$TAG Keep alive for third party accounts Service channel has been disabled, can't start foreground Service!" ) return } @@ -1818,7 +1908,7 @@ class NotificationsManager Compatibility.FOREGROUND_SERVICE_TYPE_SPECIAL_USE ) if (!success) { - Log.e("$TAG Failed to start keep alive foreground service!") + Log.e("$TAG Failed to start keep alive foreground Service!") } currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId = KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID } else { diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index f9491bfcf..a6aced1b9 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -381,6 +381,10 @@ class CallActivity : GenericActivity() { Log.w("$TAG Call activity is being resumed but no call was found, finishing activity") finish() } + + coreContext.postOnCoreThread { + coreContext.notificationsManager.showInCallForegroundServiceNotificationIfNeeded() + } } override fun onPause() { diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 73afcec00..32bcd6c37 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -266,6 +266,14 @@ class LinphoneUtils { } } + @AnyThread + fun isCallActive(callState: Call.State): Boolean { + return when (callState) { + Call.State.Connected, Call.State.StreamsRunning, Call.State.UpdatedByRemote, Call.State.Updating -> true + else -> false + } + } + @WorkerThread fun getCallErrorInfoToast(call: Call): String { val errorInfo = call.errorInfo diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9cb66ced2..212246015 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -76,6 +76,8 @@ Désactiver haut-parleur Compte %s en erreur ! Ouvrez &appName; pour rafraîchir la connexion + Votre correspondant ne vous entend pas ! + Cliquez sur cette notification pour corriger le problème Bienvenue diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fa51ca51..79a147bfd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -118,6 +118,8 @@ Turn off speaker Account %s registration failed! Open &appName; to refresh the registration + Your correspondent does not hear you! + Click on this notification to fix it Welcome