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 UI on tablets with screen sw600dp and higher, will look more like our desktop app
- Improved navigation within app when using a keyboard - Improved navigation within app when using a keyboard
- Now loading media/documents contents in conversation by chunks (instead of all of them at once) - 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 - Simplified audio device name in settings
- Reworked some settings (moved calls related ones from advanced settings to advanced calls 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 - 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" const val CHAT_TAG = "Chat"
private const val ACCOUNT_ERROR_TAG = "Account Error" 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 MISSED_CALL_TAG = "Missed call"
private const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" 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 DUMMY_NOTIF_ID = 3
private const val KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID = 5 private const val KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID = 5
private const val ACCOUNT_REGISTRATION_ERROR_ID = 7 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 private const val MISSED_CALL_ID = 10
} }
@ -226,13 +228,14 @@ class NotificationsManager
Log.i( Log.i(
"$TAG Updating incoming call notification to active call for [${call.remoteAddress.asStringUriOnly()}]" "$TAG Updating incoming call notification to active call for [${call.remoteAddress.asStringUriOnly()}]"
) )
currentlyRingingCallRemoteAddress = null
showCallNotification(call, false) showCallNotification(call, false)
} }
Call.State.StreamsRunning -> { Call.State.StreamsRunning -> {
val notifiable = getNotifiableForCall(call) val notifiable = getNotifiableForCall(call)
if (notifiable.notificationId == currentInCallServiceNotificationId) { if (notifiable.notificationId == currentInCallServiceNotificationId) {
Log.i( 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) startInCallForegroundService(call)
} }
@ -282,12 +285,17 @@ class NotificationsManager
override fun onLastCallEnded(core: Core) { override fun onLastCallEnded(core: Core) {
Log.i("$TAG Last call ended") Log.i("$TAG Last call ended")
if (inCallServiceForegroundNotificationPublished) { if (inCallServiceForegroundNotificationPublished) {
Log.i("$TAG Stopping foreground service") Log.i("$TAG Stopping foreground Service")
stopInCallForegroundService() stopInCallForegroundService()
} else { } else {
Log.i("$TAG In-Call service was never started as foreground, waiting for it to be started to stop it") Log.i("$TAG In-Call service was never started as foreground, waiting for it to be started to stop it")
waitForInCallServiceForegroundToStopIt = true 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 @WorkerThread
@ -673,6 +681,26 @@ class NotificationsManager
coreContext.contactsManager.removeListener(contactsListener) 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 @WorkerThread
fun removeIncomingCallNotificationIfAny(call: Call) { fun removeIncomingCallNotificationIfAny(call: Call) {
val notifiable = getNotifiableForCall(call) val notifiable = getNotifiableForCall(call)
@ -730,14 +758,14 @@ class NotificationsManager
if (isIncoming) { if (isIncoming) {
currentlyRingingCallRemoteAddress = call.remoteAddress currentlyRingingCallRemoteAddress = call.remoteAddress
if (currentInCallServiceNotificationId == -1) { 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) showIncomingCallForegroundServiceNotification(notifiable.notificationId, notification)
} else { } else {
notify(notifiable.notificationId, notification) notify(notifiable.notificationId, notification)
} }
} else { } else {
if (currentInCallServiceNotificationId == -1) { 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) showInCallForegroundServiceNotification(call, notifiable, notification)
} else { } else {
notify(notifiable.notificationId, notification) notify(notifiable.notificationId, notification)
@ -807,20 +835,21 @@ class NotificationsManager
notification, notification,
Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL
) )
if (!success) { if (success) {
Log.e("$TAG Failed to start incoming call foreground service!") notificationsMap[notificationId] = notification
} currentInCallServiceNotificationId = notificationId
notificationsMap[notificationId] = notification inCallServiceForegroundNotificationPublished = true
currentInCallServiceNotificationId = notificationId Log.i("$TAG Incoming call notification with ID [$notificationId] has been used to start service as foreground")
inCallServiceForegroundNotificationPublished = true
Log.i("$TAG Incoming call notification with ID [$notificationId] has been used to start service as foreground")
if (waitForInCallServiceForegroundToStopIt) { if (waitForInCallServiceForegroundToStopIt) {
Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") Log.i("$TAG We were waiting for foreground Service to be started to stop it, doing it")
stopInCallForegroundService() stopInCallForegroundService()
}
} else {
Log.e("$TAG Failed to start incoming call foreground Service!")
} }
} else { } 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 { } else {
Log.w("$TAG Core Foreground Service hasn't started yet...") Log.w("$TAG Core Foreground Service hasn't started yet...")
@ -853,7 +882,7 @@ class NotificationsManager
val channel = notificationManager.getNotificationChannel(channelId) val channel = notificationManager.getNotificationChannel(channelId)
val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE
if (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() stopInCallForegroundService()
return return
} }
@ -897,7 +926,7 @@ class NotificationsManager
) { ) {
mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_MICROPHONE mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_MICROPHONE
Log.i( 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) { val isSendingVideo = when (call.currentParams.videoDirection) {
@ -912,7 +941,7 @@ class NotificationsManager
) { ) {
mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_CAMERA mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_CAMERA
Log.i( 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, notification,
mask mask
) )
if (!success) { if (success) {
Log.e("$TAG Failed to start call foreground service!") if (notificationsMap.containsKey(IN_CALL_FOREGROUND_SERVICE_ERROR_ID)) {
} Log.i("$TAG Removing previous in-call foreground Service error notification")
notificationsMap[notifiable.notificationId] = notification cancelNotification(IN_CALL_FOREGROUND_SERVICE_ERROR_ID, IN_CALL_ERROR_TAG)
currentInCallServiceNotificationId = notifiable.notificationId }
inCallServiceForegroundNotificationPublished = true
Log.i("$TAG Call notification with ID [${notifiable.notificationId}] has been used to start service as foreground")
if (waitForInCallServiceForegroundToStopIt) { notificationsMap[notifiable.notificationId] = notification
Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") currentInCallServiceNotificationId = notifiable.notificationId
stopInCallForegroundService() 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 { } 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, notification,
Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL
) )
if (!success) { if (success) {
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 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 { } 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 { } else {
Log.w("$TAG Core Foreground Service hasn't started yet...") Log.w("$TAG Core Foreground Service hasn't started yet...")
@ -1001,13 +1046,15 @@ class NotificationsManager
private fun stopInCallForegroundService() { private fun stopInCallForegroundService() {
val service = inCallService val service = inCallService
if (service != null) { if (service != null) {
Log.i( if (currentInCallServiceNotificationId != -1) {
"$TAG Stopping foreground Service (was using notification ID [$currentInCallServiceNotificationId])" Log.i(
) "$TAG Stopping foreground Service (was using notification ID [$currentInCallServiceNotificationId])"
service.stopForeground(STOP_FOREGROUND_REMOVE) )
service.stopSelf() service.stopForeground(STOP_FOREGROUND_REMOVE)
inCallServiceForegroundNotificationPublished = false service.stopSelf()
waitForInCallServiceForegroundToStopIt = false inCallServiceForegroundNotificationPublished = false
waitForInCallServiceForegroundToStopIt = false
}
} else { } else {
Log.w("$TAG Can't stop foreground Service & notif, no Service was found") 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") @SuppressLint("MissingPermission")
@WorkerThread @WorkerThread
private fun notify(id: Int, notification: Notification, tag: String? = null) { private fun notify(id: Int, notification: Notification, tag: String? = null) {
@ -1573,7 +1658,12 @@ class NotificationsManager
val address = call.remoteAddress.asStringUriOnly() val address = call.remoteAddress.asStringUriOnly()
val notifiable: Notifiable? = callNotificationsMap[address] val notifiable: Notifiable? = callNotificationsMap[address]
if (notifiable != null) { 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) callNotificationsMap.remove(address)
} else { } else {
Log.w("$TAG No notification found for call with remote address [$address]") 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 val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE
if (importance == NotificationManagerCompat.IMPORTANCE_NONE) { if (importance == NotificationManagerCompat.IMPORTANCE_NONE) {
Log.e( 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 return
} }
@ -1818,7 +1908,7 @@ class NotificationsManager
Compatibility.FOREGROUND_SERVICE_TYPE_SPECIAL_USE Compatibility.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
) )
if (!success) { 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 currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId = KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID
} else { } 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") Log.w("$TAG Call activity is being resumed but no call was found, finishing activity")
finish() finish()
} }
coreContext.postOnCoreThread {
coreContext.notificationsManager.showInCallForegroundServiceNotificationIfNeeded()
}
} }
override fun onPause() { 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 @WorkerThread
fun getCallErrorInfoToast(call: Call): String { fun getCallErrorInfoToast(call: Call): String {
val errorInfo = call.errorInfo 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_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_title">Compte %s en erreur !</string>
<string name="notification_account_registration_error_message">Ouvrez &appName; pour rafraîchir la connexion</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 --> <!-- First screens user see when app is installed and started -->
<string name="welcome_page_title">Bienvenue</string> <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_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_title">Account %s registration failed!</string>
<string name="notification_account_registration_error_message">Open &appName; to refresh the registration</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 --> <!-- First screens user see when app is installed and started -->
<string name="welcome_page_title">Welcome</string> <string name="welcome_page_title">Welcome</string>