Browse Source

Show ongoing Android calls on the lock screen

master
evgeny 21 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 answerWhenVisible = 0L
private var activityVisible = false private var activityVisible = false
private var coreReady = false private var coreReady = false
private var startedAt = 0L
private var incomingAt = 0L private var incomingAt = 0L
private var initialized = false private var initialized = false
@ -57,7 +56,7 @@ object CallController : TelecomCallManager.Listener {
val st = _state.value ?: continue val st = _state.value ?: continue
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
if (st.phase == CallPhase.ACTIVE) { 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) { } else if (st.phase == CallPhase.INCOMING && now - incomingAt > 50_000) {
LogManager.addLog("WARN", "Call", "incoming timeout id=${st.callId}; declining") LogManager.addLog("WARN", "Call", "incoming timeout id=${st.callId}; declining")
end(st.callId) end(st.callId)
@ -297,6 +296,7 @@ object CallController : TelecomCallManager.Listener {
35 -> if (st.phase == CallPhase.OUTGOING) { 35 -> if (st.phase == CallPhase.OUTGOING) {
_state.value = st.copy(phase = CallPhase.RINGING) _state.value = st.copy(phase = CallPhase.RINGING)
CallRingback.startRinging() CallRingback.startRinging()
service?.updateNotification()
} }
36 -> { 36 -> {
if (st.phase == CallPhase.ACTIVE) return if (st.phase == CallPhase.ACTIVE) return
@ -306,12 +306,11 @@ object CallController : TelecomCallManager.Listener {
end(id, 7) end(id, 7)
return return
} }
startedAt = SystemClock.elapsedRealtime()
accepting = false accepting = false
CallRingback.stop() CallRingback.stop()
IncomingCallManager.stopRinging() IncomingCallManager.stopRinging()
TelecomCallManager.setActive(id) 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) } owner.audio.router.onChanged = { route, headset -> routeChanged(route, headset) }
_earpiece.value = owner.audio.router.hasEarpiece _earpiece.value = owner.audio.router.hasEarpiece
routeChanged(owner.audio.router.route, owner.audio.router.headsetAvailable) 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.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.os.SystemClock
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.utun.chat.ChatApplication import com.utun.chat.ChatApplication
@ -27,7 +29,8 @@ import com.utun.chat.MainActivity
*/ */
class CallService : Service() { class CallService : Service() {
companion object { 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 NOTIFICATION_ID = 1002
private const val EXTRA_ID = "call_id" private const val EXTRA_ID = "call_id"
private const val ACTION_END = "com.utun.chat.action.END_CALL" private const val ACTION_END = "com.utun.chat.action.END_CALL"
@ -53,10 +56,18 @@ class CallService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
getSystemService(NotificationManager::class.java).createNotificationChannel( val manager = getSystemService(NotificationManager::class.java)
NotificationChannel(CHANNEL, "Текущий звонок", NotificationManager.IMPORTANCE_LOW).apply { manager.createNotificationChannel(
NotificationChannel(CHANNEL, "Текущий звонок", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "Собеседник, длительность и завершение звонка на экране блокировки"
setSound(null, null) 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) } audio.onFailure = { id, detail -> CallController.audioFailed(id, detail) }
LogManager.addLog("INFO", "CallService", "created") LogManager.addLog("INFO", "CallService", "created")
} }
@ -122,6 +133,7 @@ class CallService : Service() {
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or micType ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or micType
} else 0 } else 0
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification(), types) 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 { fun startAudio(id: Long): Boolean {
@ -135,25 +147,43 @@ class CallService : Service() {
fun updateNotification() { fun updateNotification() {
getSystemService(NotificationManager::class.java).notify(NOTIFICATION_ID, notification()) 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 { 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), val open = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val end = PendingIntent.getService(this, 0, val end = PendingIntent.getService(this, 0,
Intent(this, CallService::class.java).setAction(ACTION_END) Intent(this, CallService::class.java).setAction(ACTION_END)
.setData(Uri.parse("utun-call:$callId")).putExtra(EXTRA_ID, callId), .setData(Uri.parse("utun-call:$callId")).putExtra(EXTRA_ID, callId),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) 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) .setSmallIcon(android.R.drawable.ic_menu_call)
.setContentTitle(st?.peerName ?: "vibechat") .setContentTitle(person.name)
.setContentText(if (st?.phase == CallPhase.ACTIVE) "Разговор" else "Установка звонка") .setContentText(when (st?.phase) {
CallPhase.ACTIVE -> "Разговор"
CallPhase.RINGING -> "Звоним…"
else -> "Установка звонка"
})
.setCategory(NotificationCompat.CATEGORY_CALL) .setCategory(NotificationCompat.CATEGORY_CALL)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOngoing(true) .setOngoing(true)
.setOnlyAlertOnce(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setContentIntent(open) .setContentIntent(open)
.addAction(0, "Завершить", end) .setStyle(NotificationCompat.CallStyle.forOngoingCall(person, end))
.build() .addPerson(person)
.setShowWhen(active)
.setUsesChronometer(active)
if (active && st != null) {
// Один источник начала разговора для UI и системного таймера; уведомление не обновляется каждую секунду.
builder.setWhen(System.currentTimeMillis() - (SystemClock.elapsedRealtime() - st.startedAtElapsedRealtime))
}
return builder.build()
} }
/** Идемпотентное освобождение до остановки сервиса; выполняется в main thread. */ /** Идемпотентное освобождение до остановки сервиса; выполняется в 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 reason: Int = -1,
val durationSeconds: Int = 0, val durationSeconds: Int = 0,
val bgpPath: String = "", 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 val ongoing: Boolean get() = phase != CallPhase.NONE && phase != CallPhase.ENDED
} }

Loading…
Cancel
Save