Browse Source

Show ongoing Android calls on the lock screen

master
evgeny 18 hours ago
parent
commit
b36c685cfa
  1. 7
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt
  2. 48
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallService.kt
  3. 3
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallState.kt

7
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt

@ -42,7 +42,6 @@ object CallController : TelecomCallManager.Listener {
private var answerWhenVisible = 0L
private var activityVisible = false
private var coreReady = false
private var startedAt = 0L
private var incomingAt = 0L
private var initialized = false
@ -57,7 +56,7 @@ object CallController : TelecomCallManager.Listener {
val st = _state.value ?: continue
val now = SystemClock.elapsedRealtime()
if (st.phase == CallPhase.ACTIVE) {
_state.value = st.copy(durationSeconds = ((now - startedAt) / 1000).toInt())
_state.value = st.copy(durationSeconds = ((now - st.startedAtElapsedRealtime) / 1000).toInt())
} else if (st.phase == CallPhase.INCOMING && now - incomingAt > 50_000) {
LogManager.addLog("WARN", "Call", "incoming timeout id=${st.callId}; declining")
end(st.callId)
@ -297,6 +296,7 @@ object CallController : TelecomCallManager.Listener {
35 -> if (st.phase == CallPhase.OUTGOING) {
_state.value = st.copy(phase = CallPhase.RINGING)
CallRingback.startRinging()
service?.updateNotification()
}
36 -> {
if (st.phase == CallPhase.ACTIVE) return
@ -306,12 +306,11 @@ object CallController : TelecomCallManager.Listener {
end(id, 7)
return
}
startedAt = SystemClock.elapsedRealtime()
accepting = false
CallRingback.stop()
IncomingCallManager.stopRinging()
TelecomCallManager.setActive(id)
_state.value = st.copy(phase = CallPhase.ACTIVE, durationSeconds = 0)
_state.value = st.copy(phase = CallPhase.ACTIVE, durationSeconds = 0, startedAtElapsedRealtime = SystemClock.elapsedRealtime())
owner.audio.router.onChanged = { route, headset -> routeChanged(route, headset) }
_earpiece.value = owner.audio.router.hasEarpiece
routeChanged(owner.audio.router.route, owner.audio.router.headsetAvailable)

48
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallService.kt

@ -14,7 +14,9 @@ import android.net.Uri
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.os.SystemClock
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import com.utun.chat.ChatApplication
@ -27,7 +29,8 @@ import com.utun.chat.MainActivity
*/
class CallService : Service() {
companion object {
private const val CHANNEL = "ongoing_call"
// У старого low-канала Android не позволяет повысить importance после создания.
private const val CHANNEL = "ongoing_call_lockscreen"
private const val NOTIFICATION_ID = 1002
private const val EXTRA_ID = "call_id"
private const val ACTION_END = "com.utun.chat.action.END_CALL"
@ -53,10 +56,18 @@ class CallService : Service() {
override fun onCreate() {
super.onCreate()
getSystemService(NotificationManager::class.java).createNotificationChannel(
NotificationChannel(CHANNEL, "Текущий звонок", NotificationManager.IMPORTANCE_LOW).apply {
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(
NotificationChannel(CHANNEL, "Текущий звонок", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "Собеседник, длительность и завершение звонка на экране блокировки"
setSound(null, null)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
val channel = manager.getNotificationChannel(CHANNEL)
LogManager.addLog("INFO", "CallService",
"notification channel=$CHANNEL enabled=${manager.areNotificationsEnabled()} " +
"importance=${channel.importance} lockscreenVisibility=${channel.lockscreenVisibility}")
audio.onFailure = { id, detail -> CallController.audioFailed(id, detail) }
LogManager.addLog("INFO", "CallService", "created")
}
@ -122,6 +133,7 @@ class CallService : Service() {
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or micType
} else 0
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification(), types)
LogManager.addLog("INFO", "CallService", "foreground notification id=$callId phase=${CallController.state.value?.phase}")
}
fun startAudio(id: Long): Boolean {
@ -135,25 +147,43 @@ class CallService : Service() {
fun updateNotification() {
getSystemService(NotificationManager::class.java).notify(NOTIFICATION_ID, notification())
LogManager.addLog("INFO", "CallService", "notification updated id=$callId phase=${CallController.state.value?.phase}")
}
private fun notification(): Notification {
val st = CallController.state.value
val st = CallController.state.value?.takeIf { it.callId == callId }
val active = st?.phase == CallPhase.ACTIVE
val person = Person.Builder().setName(st?.peerName?.takeIf { it.isNotBlank() } ?: "vibechat")
.setKey("node:${st?.peerNodeId}").build()
val open = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val end = PendingIntent.getService(this, 0,
Intent(this, CallService::class.java).setAction(ACTION_END)
.setData(Uri.parse("utun-call:$callId")).putExtra(EXTRA_ID, callId),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(this, CHANNEL)
val builder = NotificationCompat.Builder(this, CHANNEL)
.setSmallIcon(android.R.drawable.ic_menu_call)
.setContentTitle(st?.peerName ?: "vibechat")
.setContentText(if (st?.phase == CallPhase.ACTIVE) "Разговор" else "Установка звонка")
.setContentTitle(person.name)
.setContentText(when (st?.phase) {
CallPhase.ACTIVE -> "Разговор"
CallPhase.RINGING -> "Звоним…"
else -> "Установка звонка"
})
.setCategory(NotificationCompat.CATEGORY_CALL)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setContentIntent(open)
.addAction(0, "Завершить", end)
.build()
.setStyle(NotificationCompat.CallStyle.forOngoingCall(person, end))
.addPerson(person)
.setShowWhen(active)
.setUsesChronometer(active)
if (active && st != null) {
// Один источник начала разговора для UI и системного таймера; уведомление не обновляется каждую секунду.
builder.setWhen(System.currentTimeMillis() - (SystemClock.elapsedRealtime() - st.startedAtElapsedRealtime))
}
return builder.build()
}
/** Идемпотентное освобождение до остановки сервиса; выполняется в main thread. */

3
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallState.kt

@ -17,7 +17,8 @@ data class CallState(
val reason: Int = -1,
val durationSeconds: Int = 0,
val bgpPath: String = "",
val stats: CallStats? = null
val stats: CallStats? = null,
val startedAtElapsedRealtime: Long = 0
) {
val ongoing: Boolean get() = phase != CallPhase.NONE && phase != CallPhase.ENDED
}

Loading…
Cancel
Save