diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index afac0c32..ed8fdc7f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -1089,6 +1089,7 @@ class BaresipService: Service() { Handler(Looper.getMainLooper()).postDelayed({ connection.destroy() ConnectionService.connections.remove(callp) + updateStatusNotification() }, 500) } nm.cancel(CALL_NOTIFICATION_ID) @@ -1565,117 +1566,142 @@ class BaresipService: Service() { fun updateStatusNotification() { - val activeCall = Call.calls().find { - it.status.value == "connected" || it.status.value == "outgoing" || it.status.value == "answered" - } - - val builder = NotificationCompat.Builder(this, LOW_CHANNEL_ID) - val intent = Intent(applicationContext, MainActivity::class.java) - .setAction(Intent.ACTION_MAIN) - .addCategory(Intent.CATEGORY_LAUNCHER) - - val pi = PendingIntent.getActivity(applicationContext, STATUS_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE) - val deleteIntent = Intent(this, BaresipService::class.java).setAction("Notification Dismissed") - val dpi = PendingIntent.getService(this, STATUS_REQ_CODE, deleteIntent, PendingIntent.FLAG_IMMUTABLE) - - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setSmallIcon(R.drawable.ic_notification_b) - .setContentIntent(pi) - .setDeleteIntent(dpi) - .setOngoing(true) - - if (activeCall != null) { - val peerUri = activeCall.peerUri - val caller = Utils.friendlyUri(this, peerUri, activeCall.ua.account) - val person = Person.Builder().setName(caller).build() - - val hangupIntent = Intent(this, BaresipService::class.java) - hangupIntent.action = "Call Hangup" - hangupIntent.putExtra("callp", activeCall.callp) - val hpi = PendingIntent.getService(this, REJECT_REQ_CODE, hangupIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - - builder.setStyle(NotificationCompat.CallStyle.forOngoingCall(person, hpi)) - .setCategory(Notification.CATEGORY_CALL) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setWhen(activeCall.startTime?.timeInMillis ?: System.currentTimeMillis()) - .setUsesChronometer(true) - .setContentText( - if (activeCall.onhold) - getString(R.string.call_is_on_hold) - else - when (activeCall.status.value) { - "outgoing", "incoming" -> getString(R.string.call_is_ringing) - "connected", "answered" -> getString(R.string.call_is_connected) - else -> getString(R.string.call) - } - ) - } else { - builder.setStyle(null) - builder.setStyle(NotificationCompat.DecoratedCustomViewStyle()) - .setCategory(Notification.CATEGORY_SERVICE) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setWhen(0) - .setShowWhen(false) - .setUsesChronometer(false) - .setContentTitle("") - .setContentText("") - - val notificationLayout = RemoteViews(packageName, R.layout.status_notification) - for (i in 0..3) { - val resId = when (i) { - 0 -> R.id.status0 - 1 -> R.id.status1 - 2 -> R.id.status2 - else -> R.id.status3 - } - if (i < uas.value.size) { - notificationLayout.setImageViewResource(resId, uas.value[i].status) - notificationLayout.setViewVisibility(resId, View.VISIBLE) - } else { - notificationLayout.setViewVisibility(resId, View.INVISIBLE) + Handler(Looper.getMainLooper()).post { + val activeCall = synchronized(calls) { + calls.find { + it.status.value == "connected" || it.status.value == "outgoing" || it.status.value == "answered" } } - if (uas.value.size > 4) - notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE) - else - notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE) - builder.setCustomContentView(notificationLayout) - } + val builder = NotificationCompat.Builder(this, LOW_CHANNEL_ID) + val intent = Intent(applicationContext, MainActivity::class.java) + .setAction(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) - if (VERSION.SDK_INT >= 31) - builder.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE + val pi = PendingIntent.getActivity( + applicationContext, + STATUS_REQ_CODE, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + val deleteIntent = + Intent(this, BaresipService::class.java).setAction("Notification Dismissed") + val dpi = PendingIntent.getService( + this, + STATUS_REQ_CODE, + deleteIntent, + PendingIntent.FLAG_IMMUTABLE + ) - builder.setOngoing(true) - val notification = builder.build() - notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or Notification.FLAG_ONGOING_EVENT + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setSmallIcon(R.drawable.ic_notification_b) + .setContentIntent(pi) + .setDeleteIntent(dpi) + .setOngoing(true) - try { if (activeCall != null) { - if (VERSION.SDK_INT >= 29) - startForeground(STATUS_NOTIFICATION_ID, notification, foregroundServiceType(activeCall)) - else - startForeground(STATUS_NOTIFICATION_ID, notification) - isNotificationInCall = true + val peerUri = activeCall.peerUri + val caller = Utils.friendlyUri(this, peerUri, activeCall.ua.account) + val person = Person.Builder().setName(caller).build() + + val hangupIntent = Intent(this, BaresipService::class.java) + hangupIntent.action = "Call Hangup" + hangupIntent.putExtra("callp", activeCall.callp) + val hpi = PendingIntent.getService( + this, REJECT_REQ_CODE, hangupIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + builder.setStyle(NotificationCompat.CallStyle.forOngoingCall(person, hpi)) + .setCategory(Notification.CATEGORY_CALL) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setWhen(activeCall.startTime?.timeInMillis ?: System.currentTimeMillis()) + .setUsesChronometer(true) + .setContentText( + if (activeCall.onhold) + getString(R.string.call_is_on_hold) + else + when (activeCall.status.value) { + "outgoing", "incoming" -> getString(R.string.call_is_ringing) + "connected", "answered" -> getString(R.string.call_is_connected) + else -> getString(R.string.call) + } + ) } else { - // If we are currently in a call notification mode, downgrade to standby FGS - // but DO NOT call stopForeground as that would drop FGS status. - if (isNotificationInCall) { + builder.setStyle(null) + builder.setStyle(NotificationCompat.DecoratedCustomViewStyle()) + .setCategory(Notification.CATEGORY_SERVICE) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setWhen(0) + .setShowWhen(false) + .setUsesChronometer(false) + .setContentTitle("") + .setContentText("") + + val notificationLayout = RemoteViews(packageName, R.layout.status_notification) + for (i in 0..3) { + val resId = when (i) { + 0 -> R.id.status0 + 1 -> R.id.status1 + 2 -> R.id.status2 + else -> R.id.status3 + } + if (i < uas.value.size) { + notificationLayout.setImageViewResource(resId, uas.value[i].status) + notificationLayout.setViewVisibility(resId, View.VISIBLE) + } else { + notificationLayout.setViewVisibility(resId, View.INVISIBLE) + } + } + if (uas.value.size > 4) + notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE) + else + notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE) + + builder.setCustomContentView(notificationLayout) + } + + if (VERSION.SDK_INT >= 31) + builder.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE + + builder.setOngoing(true) + val notification = builder.build() + notification.flags = + notification.flags or Notification.FLAG_NO_CLEAR or Notification.FLAG_ONGOING_EVENT + + try { + if (activeCall != null) { if (VERSION.SDK_INT >= 29) - startForeground(STATUS_NOTIFICATION_ID, notification, foregroundServiceType()) + startForeground( + STATUS_NOTIFICATION_ID, + notification, + foregroundServiceType(activeCall) + ) else startForeground(STATUS_NOTIFICATION_ID, notification) - isNotificationInCall = false + isNotificationInCall = true } else { - // Already in standby, just update the notification content - nm.notify(STATUS_NOTIFICATION_ID, notification) + // If we are currently in a call notification mode, downgrade to standby FGS. + // We call stopForeground(REMOVE) to explicitly clear the system's telephony UI context + // that might be persisting the timer/peer info on the lock screen. + if (isNotificationInCall) { + stopForeground(STOP_FOREGROUND_REMOVE) + isNotificationInCall = false + } + if (VERSION.SDK_INT >= 29) + startForeground( + STATUS_NOTIFICATION_ID, + notification, + foregroundServiceType() + ) + else + startForeground(STATUS_NOTIFICATION_ID, notification) } + } catch (e: Exception) { + Log.e(TAG, "Failed to update foreground notification: ${e.message}") + // Fallback to standard notification if promotion/update fails + nm.notify(STATUS_NOTIFICATION_ID, notification) } - } catch (e: Exception) { - Log.e(TAG, "Failed to update foreground notification: ${e.message}") - // Fallback to standard notification if promotion/update fails - nm.notify(STATUS_NOTIFICATION_ID, notification) } } @@ -1841,6 +1867,7 @@ class BaresipService: Service() { proximitySensing(false) stopMediaPlayer() } + updateStatusNotification() messageUpdate.postValue(System.currentTimeMillis()) } diff --git a/app/src/main/kotlin/com/tutpro/baresip/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/Call.kt index 79aa72c6..8dce9f3f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Call.kt @@ -48,11 +48,15 @@ open class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir val focusDtmf: MutableState = mutableStateOf(false) fun add() { - BaresipService.calls.add(this) + synchronized(BaresipService.calls) { + BaresipService.calls.add(this) + } } fun remove() { - BaresipService.calls.remove(this) + synchronized(BaresipService.calls) { + BaresipService.calls.remove(this) + } } open fun connect(uri: String): Boolean { diff --git a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt index 64b4b862..6275aa96 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt @@ -195,6 +195,7 @@ class ConnectionService : ConnectionService() { if (call != null) Api.ua_hangup(uap, callp, 0, "") setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) + connections.remove(callp) destroy() // Allow other disconnects after a short period to prevent the "Telecom Cascade" effect android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({