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

This commit is contained in:
Sylvain Berfini 2026-02-23 09:56:29 +01:00
parent 1f36852f37
commit 37200ecf8f
6 changed files with 153 additions and 46 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -76,6 +76,8 @@
<string name="notification_disable_speaker_for_call">Désactiver haut-parleur</string>
<string name="notification_account_registration_error_title">Compte %s en erreur !</string>
<string name="notification_account_registration_error_message">Ouvrez &appName; pour rafraîchir la connexion</string>
<string name="notification_in_call_foreground_service_error_title">Votre correspondant ne vous entend pas !</string>
<string name="notification_in_call_foreground_service_error_message">Cliquez sur cette notification pour corriger le problème</string>
<!-- First screens user see when app is installed and started -->
<string name="welcome_page_title">Bienvenue</string>

View file

@ -118,6 +118,8 @@
<string name="notification_disable_speaker_for_call">Turn off speaker</string>
<string name="notification_account_registration_error_title">Account %s registration failed!</string>
<string name="notification_account_registration_error_message">Open &appName; to refresh the registration</string>
<string name="notification_in_call_foreground_service_error_title">Your correspondent does not hear you!</string>
<string name="notification_in_call_foreground_service_error_message">Click on this notification to fix it</string>
<!-- First screens user see when app is installed and started -->
<string name="welcome_page_title">Welcome</string>