Show ongoing Android calls on the lock screen
This commit is contained in:
1 parent
3c5c9604b3
commit
b36c685cfa
3 files changed
+44
-14
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user