Compare commits
2 Commits
7d4b9a8fc4
...
9e14724ae2
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e14724ae2 | |||
| 2bb3281ccf |
@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// Rosetta versioning — bump here on each release
|
// Rosetta versioning — bump here on each release
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val rosettaVersionName = "1.4.5"
|
val rosettaVersionName = "1.4.6"
|
||||||
val rosettaVersionCode = 47 // Increment on each release
|
val rosettaVersionCode = 48 // Increment on each release
|
||||||
val customWebRtcAar = file("libs/libwebrtc-custom.aar")
|
val customWebRtcAar = file("libs/libwebrtc-custom.aar")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -18,31 +18,15 @@ object ReleaseNotes {
|
|||||||
Update v$VERSION_PLACEHOLDER
|
Update v$VERSION_PLACEHOLDER
|
||||||
|
|
||||||
Звонки
|
Звонки
|
||||||
- Полноэкранный входящий звонок (IncomingCallActivity) поверх экрана блокировки с кнопками Принять/Отклонить
|
- Android переведён на новый серверный сигналинг звонков: CALL -> ACCEPT -> KEY_EXCHANGE -> ACTIVE
|
||||||
- Обновлён протокол WebRTC: publicKey и deviceId в каждом пакете (совместимость с новым сервером)
|
- Для звонков добавлена полная поддержка callId/joinToken (в CALL/ACCEPT/END_CALL)
|
||||||
- Звонок больше не сбрасывается при переподключении WebSocket
|
- Добавлена обработка RINGING_TIMEOUT с корректным завершением звонка
|
||||||
- Исправлен бесконечный статус "Exchanging keys" — KEY_EXCHANGE отправляется с ретраем до 6 сек
|
- WebRTC пакет 0x1B обновлён под новый формат сервера (без лишних полей в payload)
|
||||||
- Автоматическая привязка аккаунта при принятии звонка из push-уведомления
|
- Push звонка теперь пробрасывает callId/joinToken в CallManager для стабильного принятия до WebSocket
|
||||||
- Исправлен краш ForegroundService при исходящем звонке (safeStopForeground)
|
|
||||||
- Убрано мелькание "Unknown" при завершении звонка
|
|
||||||
- Кнопка Decline теперь работает во всех фазах звонка
|
|
||||||
- Баннер активного звонка теперь отображается внутри диалога
|
|
||||||
- Дедупликация push + WebSocket сигналов (без мерцания уведомлений)
|
|
||||||
- Защита от фантомных звонков при принятии на другом устройстве
|
|
||||||
- Корректное освобождение PeerConnection (dispose) при завершении звонка
|
|
||||||
- Кастомный WebRTC AAR с E2EE добавлен в репозиторий для CI-сборок
|
|
||||||
- Диагностические логи звонков и уведомлений в rosettadev1
|
|
||||||
|
|
||||||
Уведомления
|
Стабильность
|
||||||
- Аватарки и имена пользователей в уведомлениях о сообщениях и звонках
|
- Улучшены диагностические логи звонков (callId/joinToken в state/log)
|
||||||
- Настройка включения/выключения аватарок в уведомлениях (Notifications → Avatars in Notifications)
|
- Обновлена совместимость Android с актуальными версиями desktop и rosetta-wss
|
||||||
- Сохранение FCM токена в rosettadev1 для диагностики
|
|
||||||
- Поддержка tokenType и deviceId в push-подписке
|
|
||||||
|
|
||||||
Интерфейс
|
|
||||||
- Ограничение масштаба шрифта до 1.3x — вёрстка не ломается на телефонах с огромным текстом
|
|
||||||
- Новые обои: Light 1-3 для светлой темы, Dark 1-3 для тёмной темы
|
|
||||||
- Убраны старые обои, исправлено растяжение превью обоев
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
fun getNotice(version: String): String =
|
fun getNotice(version: String): String =
|
||||||
|
|||||||
@@ -110,13 +110,14 @@ object CallManager {
|
|||||||
private var ownPublicKey: String = ""
|
private var ownPublicKey: String = ""
|
||||||
|
|
||||||
private var role: CallRole? = null
|
private var role: CallRole? = null
|
||||||
private var roomId: String = ""
|
private var serverCallId: String = ""
|
||||||
|
private var serverJoinToken: String = ""
|
||||||
private var offerSent = false
|
private var offerSent = false
|
||||||
private var remoteDescriptionSet = false
|
private var remoteDescriptionSet = false
|
||||||
private var callSessionId: String = ""
|
private var callSessionId: String = ""
|
||||||
private var callStartedAtMs: Long = 0L
|
private var callStartedAtMs: Long = 0L
|
||||||
private var keyExchangeSent = false
|
private var keyExchangeSent = false
|
||||||
private var createRoomSent = false
|
private var activeSignalSent = false
|
||||||
private var lastPeerSharedPublicHex: String = ""
|
private var lastPeerSharedPublicHex: String = ""
|
||||||
|
|
||||||
private var localPrivateKey: ByteArray? = null
|
private var localPrivateKey: ByteArray? = null
|
||||||
@@ -183,7 +184,8 @@ object CallManager {
|
|||||||
if (phase == CallPhase.IDLE) {
|
if (phase == CallPhase.IDLE) {
|
||||||
val hasResidualSession =
|
val hasResidualSession =
|
||||||
callSessionId.isNotBlank() ||
|
callSessionId.isNotBlank() ||
|
||||||
roomId.isNotBlank() ||
|
serverCallId.isNotBlank() ||
|
||||||
|
serverJoinToken.isNotBlank() ||
|
||||||
role != null ||
|
role != null ||
|
||||||
_state.value.peerPublicKey.isNotBlank() ||
|
_state.value.peerPublicKey.isNotBlank() ||
|
||||||
sharedKeyBytes != null ||
|
sharedKeyBytes != null ||
|
||||||
@@ -214,7 +216,12 @@ object CallManager {
|
|||||||
* Ставит CallManager в INCOMING сразу, не дожидаясь WebSocket сигнала.
|
* Ставит CallManager в INCOMING сразу, не дожидаясь WebSocket сигнала.
|
||||||
* Если WebSocket CALL придёт позже — дедупликация его отбросит.
|
* Если WebSocket CALL придёт позже — дедупликация его отбросит.
|
||||||
*/
|
*/
|
||||||
fun setIncomingFromPush(peerPublicKey: String, peerTitle: String) {
|
fun setIncomingFromPush(
|
||||||
|
peerPublicKey: String,
|
||||||
|
peerTitle: String,
|
||||||
|
callId: String = "",
|
||||||
|
joinToken: String = ""
|
||||||
|
) {
|
||||||
val peer = peerPublicKey.trim()
|
val peer = peerPublicKey.trim()
|
||||||
if (peer.isBlank()) return
|
if (peer.isBlank()) return
|
||||||
// Уже в звонке — не перебиваем
|
// Уже в звонке — не перебиваем
|
||||||
@@ -222,7 +229,12 @@ object CallManager {
|
|||||||
breadcrumb("setIncomingFromPush SKIP: phase=${_state.value.phase}")
|
breadcrumb("setIncomingFromPush SKIP: phase=${_state.value.phase}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
breadcrumb("setIncomingFromPush peer=${peer.take(8)}… title=$peerTitle")
|
serverCallId = callId.trim()
|
||||||
|
serverJoinToken = joinToken.trim()
|
||||||
|
breadcrumb(
|
||||||
|
"setIncomingFromPush peer=${peer.take(8)}… title=$peerTitle " +
|
||||||
|
"callId=${serverCallId.take(12)} join=${serverJoinToken.take(12)}"
|
||||||
|
)
|
||||||
beginCallSession("incoming-push:${peer.take(8)}")
|
beginCallSession("incoming-push:${peer.take(8)}")
|
||||||
role = CallRole.CALLEE
|
role = CallRole.CALLEE
|
||||||
resetRtcObjects()
|
resetRtcObjects()
|
||||||
@@ -299,7 +311,6 @@ object CallManager {
|
|||||||
|
|
||||||
role = CallRole.CALLEE
|
role = CallRole.CALLEE
|
||||||
generateSessionKeys()
|
generateSessionKeys()
|
||||||
val localPublic = localPublicKey ?: return CallActionResult.INVALID_TARGET
|
|
||||||
|
|
||||||
incomingRingTimeoutJob?.cancel()
|
incomingRingTimeoutJob?.cancel()
|
||||||
incomingRingTimeoutJob = null
|
incomingRingTimeoutJob = null
|
||||||
@@ -312,19 +323,30 @@ object CallManager {
|
|||||||
}
|
}
|
||||||
armConnectingTimeout("acceptIncomingCall")
|
armConnectingTimeout("acceptIncomingCall")
|
||||||
|
|
||||||
// Отправляем KEY_EXCHANGE — если WebSocket не подключен, ждём и ретраим
|
// Отправляем ACCEPT с callId/joinToken. Если push пришел раньше WS CALL,
|
||||||
|
// подождем немного пока идентификаторы звонка подтянутся.
|
||||||
scope.launch {
|
scope.launch {
|
||||||
var sent = false
|
var sent = false
|
||||||
for (attempt in 1..30) { // 30 * 200ms = 6 sec
|
for (attempt in 1..30) { // 30 * 200ms = 6 sec
|
||||||
|
val callIdNow = serverCallId.trim()
|
||||||
|
val joinTokenNow = serverJoinToken.trim()
|
||||||
|
if (callIdNow.isBlank() || joinTokenNow.isBlank()) {
|
||||||
|
breadcrumb("acceptIncomingCall: waiting callId/joinToken (attempt #$attempt)")
|
||||||
|
kotlinx.coroutines.delay(200)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (ProtocolManager.isAuthenticated()) {
|
if (ProtocolManager.isAuthenticated()) {
|
||||||
ProtocolManager.sendCallSignal(
|
ProtocolManager.sendCallSignal(
|
||||||
signalType = SignalType.KEY_EXCHANGE,
|
signalType = SignalType.ACCEPT,
|
||||||
src = ownPublicKey,
|
src = ownPublicKey,
|
||||||
dst = snapshot.peerPublicKey,
|
dst = snapshot.peerPublicKey,
|
||||||
sharedPublic = localPublic.toHex()
|
callId = callIdNow,
|
||||||
|
joinToken = joinTokenNow
|
||||||
|
)
|
||||||
|
breadcrumb(
|
||||||
|
"acceptIncomingCall: ACCEPT sent (attempt #$attempt) " +
|
||||||
|
"callId=${callIdNow.take(12)} join=${joinTokenNow.take(12)}"
|
||||||
)
|
)
|
||||||
keyExchangeSent = true
|
|
||||||
breadcrumb("acceptIncomingCall: KEY_EXCHANGE sent (attempt #$attempt)")
|
|
||||||
sent = true
|
sent = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -332,7 +354,7 @@ object CallManager {
|
|||||||
kotlinx.coroutines.delay(200)
|
kotlinx.coroutines.delay(200)
|
||||||
}
|
}
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
breadcrumb("acceptIncomingCall: FAILED to send KEY_EXCHANGE after 6s — resetting")
|
breadcrumb("acceptIncomingCall: FAILED to send ACCEPT after 6s — resetting")
|
||||||
resetSession(reason = "Failed to connect", notifyPeer = false)
|
resetSession(reason = "Failed to connect", notifyPeer = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,11 +368,20 @@ object CallManager {
|
|||||||
if (snapshot.phase != CallPhase.INCOMING) return
|
if (snapshot.phase != CallPhase.INCOMING) return
|
||||||
incomingRingTimeoutJob?.cancel()
|
incomingRingTimeoutJob?.cancel()
|
||||||
incomingRingTimeoutJob = null
|
incomingRingTimeoutJob = null
|
||||||
if (ownPublicKey.isNotBlank() && snapshot.peerPublicKey.isNotBlank()) {
|
val callIdNow = serverCallId.trim()
|
||||||
|
val joinTokenNow = serverJoinToken.trim()
|
||||||
|
if (ownPublicKey.isNotBlank() && snapshot.peerPublicKey.isNotBlank() && callIdNow.isNotBlank() && joinTokenNow.isNotBlank()) {
|
||||||
ProtocolManager.sendCallSignal(
|
ProtocolManager.sendCallSignal(
|
||||||
signalType = SignalType.END_CALL,
|
signalType = SignalType.END_CALL,
|
||||||
src = ownPublicKey,
|
src = ownPublicKey,
|
||||||
dst = snapshot.peerPublicKey
|
dst = snapshot.peerPublicKey,
|
||||||
|
callId = callIdNow,
|
||||||
|
joinToken = joinTokenNow
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
breadcrumb(
|
||||||
|
"declineIncomingCall: skip END_CALL (missing ids) " +
|
||||||
|
"callId=${callIdNow.take(12)} join=${joinTokenNow.take(12)}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
resetSession(reason = null, notifyPeer = false)
|
resetSession(reason = null, notifyPeer = false)
|
||||||
@@ -400,6 +431,11 @@ object CallManager {
|
|||||||
resetSession(reason = "Peer disconnected", notifyPeer = false)
|
resetSession(reason = "Peer disconnected", notifyPeer = false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
SignalType.RINGING_TIMEOUT -> {
|
||||||
|
breadcrumb("SIG: ringing timeout → reset")
|
||||||
|
resetSession(reason = "No answer", notifyPeer = false)
|
||||||
|
return
|
||||||
|
}
|
||||||
SignalType.END_CALL -> {
|
SignalType.END_CALL -> {
|
||||||
breadcrumb("SIG: END_CALL → reset")
|
breadcrumb("SIG: END_CALL → reset")
|
||||||
resetSession(reason = "Call ended", notifyPeer = false)
|
resetSession(reason = "Call ended", notifyPeer = false)
|
||||||
@@ -419,10 +455,15 @@ object CallManager {
|
|||||||
SignalType.CALL -> {
|
SignalType.CALL -> {
|
||||||
val incomingPeer = packet.src.trim()
|
val incomingPeer = packet.src.trim()
|
||||||
if (incomingPeer.isBlank()) return
|
if (incomingPeer.isBlank()) return
|
||||||
|
serverCallId = packet.callId.trim()
|
||||||
|
serverJoinToken = packet.joinToken.trim()
|
||||||
|
|
||||||
// Дедупликация: push уже поставил INCOMING для этого peer — обновляем только имя
|
// Дедупликация: push уже поставил INCOMING для этого peer — обновляем только имя
|
||||||
if (_state.value.phase == CallPhase.INCOMING && _state.value.peerPublicKey == incomingPeer) {
|
if (_state.value.phase == CallPhase.INCOMING && _state.value.peerPublicKey == incomingPeer) {
|
||||||
breadcrumb("SIG: CALL from ${incomingPeer.take(8)}… but already INCOMING — dedup")
|
breadcrumb(
|
||||||
|
"SIG: CALL from ${incomingPeer.take(8)}… but already INCOMING — dedup " +
|
||||||
|
"callId=${serverCallId.take(12)} join=${serverJoinToken.take(12)}"
|
||||||
|
)
|
||||||
resolvePeerIdentity(incomingPeer)
|
resolvePeerIdentity(incomingPeer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -439,7 +480,10 @@ object CallManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
beginCallSession("incoming:${incomingPeer.take(8)}")
|
beginCallSession("incoming:${incomingPeer.take(8)}")
|
||||||
breadcrumb("SIG: CALL from ${incomingPeer.take(8)}… → INCOMING")
|
breadcrumb(
|
||||||
|
"SIG: CALL from ${incomingPeer.take(8)}… → INCOMING " +
|
||||||
|
"callId=${serverCallId.take(12)} join=${serverJoinToken.take(12)}"
|
||||||
|
)
|
||||||
role = CallRole.CALLEE
|
role = CallRole.CALLEE
|
||||||
resetRtcObjects()
|
resetRtcObjects()
|
||||||
// Пробуем сразу взять имя из кэша чтобы ForegroundService показал его
|
// Пробуем сразу взять имя из кэша чтобы ForegroundService показал его
|
||||||
@@ -490,29 +534,51 @@ object CallManager {
|
|||||||
breadcrumb("SIG: KEY_EXCHANGE → handleKeyExchange")
|
breadcrumb("SIG: KEY_EXCHANGE → handleKeyExchange")
|
||||||
handleKeyExchange(packet)
|
handleKeyExchange(packet)
|
||||||
}
|
}
|
||||||
SignalType.CREATE_ROOM -> {
|
SignalType.ACCEPT -> {
|
||||||
val incomingRoomId = packet.roomId.trim()
|
breadcrumb(
|
||||||
breadcrumb("SIG: CREATE_ROOM roomId=${incomingRoomId.take(16)}…")
|
"SIG: ACCEPT callId=${packet.callId.take(12)} join=${packet.joinToken.take(12)}"
|
||||||
if (incomingRoomId.isBlank()) {
|
)
|
||||||
breadcrumb("SIG: CREATE_ROOM IGNORED — empty roomId!")
|
serverCallId = packet.callId.trim()
|
||||||
|
serverJoinToken = packet.joinToken.trim()
|
||||||
|
if (role != CallRole.CALLER) {
|
||||||
|
breadcrumb("SIG: ACCEPT ignored — role=$role")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Если ключей нет — звонок был принят на другом устройстве,
|
if (localPrivateKey == null || localPublicKey == null) {
|
||||||
// а сервер всё равно прислал CREATE_ROOM. Сбрасываем.
|
breadcrumb("SIG: ACCEPT — generating local session keys")
|
||||||
|
generateSessionKeys()
|
||||||
|
}
|
||||||
|
val localPublic = localPublicKey ?: return
|
||||||
|
ProtocolManager.sendCallSignal(
|
||||||
|
signalType = SignalType.KEY_EXCHANGE,
|
||||||
|
src = ownPublicKey,
|
||||||
|
dst = _state.value.peerPublicKey,
|
||||||
|
sharedPublic = localPublic.toHex()
|
||||||
|
)
|
||||||
|
keyExchangeSent = true
|
||||||
|
updateState {
|
||||||
|
it.copy(
|
||||||
|
phase = CallPhase.CONNECTING,
|
||||||
|
statusText = "Exchanging keys..."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
armConnectingTimeout("signal:accept")
|
||||||
|
}
|
||||||
|
SignalType.ACTIVE -> {
|
||||||
|
breadcrumb("SIG: ACTIVE")
|
||||||
if (sharedKeyBytes == null && localPrivateKey == null) {
|
if (sharedKeyBytes == null && localPrivateKey == null) {
|
||||||
breadcrumb("SIG: CREATE_ROOM but no session keys — call accepted on another device, resetting")
|
breadcrumb("SIG: ACTIVE but no session keys — resetting")
|
||||||
CallSoundManager.stop()
|
CallSoundManager.stop()
|
||||||
resetSession(reason = null, notifyPeer = false)
|
resetSession(reason = null, notifyPeer = false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
roomId = incomingRoomId
|
|
||||||
updateState {
|
updateState {
|
||||||
it.copy(
|
it.copy(
|
||||||
phase = CallPhase.CONNECTING,
|
phase = CallPhase.CONNECTING,
|
||||||
statusText = "Connecting..."
|
statusText = "Connecting..."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
armConnectingTimeout("signal:create_room")
|
armConnectingTimeout("signal:active")
|
||||||
ensurePeerConnectionAndOffer()
|
ensurePeerConnectionAndOffer()
|
||||||
}
|
}
|
||||||
SignalType.ACTIVE_CALL -> Unit
|
SignalType.ACTIVE_CALL -> Unit
|
||||||
@@ -552,25 +618,15 @@ object CallManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setupE2EE(sharedKey)
|
setupE2EE(sharedKey)
|
||||||
breadcrumb("KE: CALLER — E2EE ready, sending missing signaling packets")
|
breadcrumb("KE: CALLER — E2EE ready, notifying ACTIVE")
|
||||||
updateState { it.copy(keyCast = sharedKey, statusText = "Creating room...") }
|
updateState { it.copy(keyCast = sharedKey, statusText = "Connecting...") }
|
||||||
val localPublic = localPublicKey ?: return
|
if (!activeSignalSent) {
|
||||||
if (!keyExchangeSent) {
|
|
||||||
ProtocolManager.sendCallSignal(
|
ProtocolManager.sendCallSignal(
|
||||||
signalType = SignalType.KEY_EXCHANGE,
|
signalType = SignalType.ACTIVE,
|
||||||
src = ownPublicKey,
|
|
||||||
dst = peerKey,
|
|
||||||
sharedPublic = localPublic.toHex()
|
|
||||||
)
|
|
||||||
keyExchangeSent = true
|
|
||||||
}
|
|
||||||
if (!createRoomSent) {
|
|
||||||
ProtocolManager.sendCallSignal(
|
|
||||||
signalType = SignalType.CREATE_ROOM,
|
|
||||||
src = ownPublicKey,
|
src = ownPublicKey,
|
||||||
dst = peerKey
|
dst = peerKey
|
||||||
)
|
)
|
||||||
createRoomSent = true
|
activeSignalSent = true
|
||||||
}
|
}
|
||||||
updateState { it.copy(phase = CallPhase.CONNECTING) }
|
updateState { it.copy(phase = CallPhase.CONNECTING) }
|
||||||
armConnectingTimeout("key_exchange:caller")
|
armConnectingTimeout("key_exchange:caller")
|
||||||
@@ -588,10 +644,23 @@ object CallManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setupE2EE(sharedKey)
|
setupE2EE(sharedKey)
|
||||||
breadcrumb("KE: CALLEE — E2EE ready, waiting for CREATE_ROOM")
|
if (!keyExchangeSent) {
|
||||||
|
val localPublic = localPublicKey ?: return
|
||||||
|
ProtocolManager.sendCallSignal(
|
||||||
|
signalType = SignalType.KEY_EXCHANGE,
|
||||||
|
src = ownPublicKey,
|
||||||
|
dst = peerKey,
|
||||||
|
sharedPublic = localPublic.toHex()
|
||||||
|
)
|
||||||
|
keyExchangeSent = true
|
||||||
|
}
|
||||||
|
breadcrumb("KE: CALLEE — E2EE ready, waiting for ACTIVE")
|
||||||
updateState { it.copy(keyCast = sharedKey, phase = CallPhase.CONNECTING) }
|
updateState { it.copy(keyCast = sharedKey, phase = CallPhase.CONNECTING) }
|
||||||
armConnectingTimeout("key_exchange:callee")
|
armConnectingTimeout("key_exchange:callee")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
breadcrumb("KE: ignored — unknown role")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleWebRtcPacket(packet: PacketWebRTC) {
|
private suspend fun handleWebRtcPacket(packet: PacketWebRTC) {
|
||||||
@@ -710,8 +779,8 @@ object CallManager {
|
|||||||
|
|
||||||
private suspend fun ensurePeerConnectionAndOffer() {
|
private suspend fun ensurePeerConnectionAndOffer() {
|
||||||
val peerKey = _state.value.peerPublicKey
|
val peerKey = _state.value.peerPublicKey
|
||||||
if (peerKey.isBlank() || roomId.isBlank()) {
|
if (peerKey.isBlank()) {
|
||||||
breadcrumb("PC: ensurePCAndOffer SKIP — peer=${peerKey.take(8)}… room=${roomId.take(8)}…")
|
breadcrumb("PC: ensurePCAndOffer SKIP — peer=${peerKey.take(8)}…")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (offerSent) {
|
if (offerSent) {
|
||||||
@@ -719,7 +788,7 @@ object CallManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
breadcrumb("PC: ensurePCAndOffer START role=$role room=${roomId.take(8)}…")
|
breadcrumb("PC: ensurePCAndOffer START role=$role")
|
||||||
ensurePeerFactory()
|
ensurePeerFactory()
|
||||||
val factory = peerConnectionFactory
|
val factory = peerConnectionFactory
|
||||||
if (factory == null) {
|
if (factory == null) {
|
||||||
@@ -897,8 +966,8 @@ object CallManager {
|
|||||||
val snapshot = _state.value
|
val snapshot = _state.value
|
||||||
if (snapshot.phase != CallPhase.CONNECTING) return@launch
|
if (snapshot.phase != CallPhase.CONNECTING) return@launch
|
||||||
breadcrumb(
|
breadcrumb(
|
||||||
"CONNECTING TIMEOUT origin=$origin role=$role room=${roomId.take(12)} " +
|
"CONNECTING TIMEOUT origin=$origin role=$role callId=${serverCallId.take(12)} join=${serverJoinToken.take(12)} " +
|
||||||
"keyExSent=$keyExchangeSent createRoomSent=$createRoomSent offerSent=$offerSent " +
|
"keyExSent=$keyExchangeSent activeSent=$activeSignalSent offerSent=$offerSent " +
|
||||||
"remoteDesc=$remoteDescriptionSet peer=${snapshot.peerPublicKey.take(8)}…"
|
"remoteDesc=$remoteDescriptionSet peer=${snapshot.peerPublicKey.take(8)}…"
|
||||||
)
|
)
|
||||||
resetSession(reason = "Connecting timeout", notifyPeer = false)
|
resetSession(reason = "Connecting timeout", notifyPeer = false)
|
||||||
@@ -983,7 +1052,9 @@ object CallManager {
|
|||||||
ProtocolManager.sendCallSignal(
|
ProtocolManager.sendCallSignal(
|
||||||
signalType = SignalType.END_CALL,
|
signalType = SignalType.END_CALL,
|
||||||
src = ownPublicKey,
|
src = ownPublicKey,
|
||||||
dst = peerToNotify
|
dst = peerToNotify,
|
||||||
|
callId = serverCallId,
|
||||||
|
joinToken = serverJoinToken
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Отменяем все jobs ПЕРВЫМИ — чтобы они не вызвали updateState с пустым state
|
// Отменяем все jobs ПЕРВЫМИ — чтобы они не вызвали updateState с пустым state
|
||||||
@@ -1011,11 +1082,12 @@ object CallManager {
|
|||||||
lastHealthLog = ""
|
lastHealthLog = ""
|
||||||
healthLogCount = 0
|
healthLogCount = 0
|
||||||
role = null
|
role = null
|
||||||
roomId = ""
|
serverCallId = ""
|
||||||
|
serverJoinToken = ""
|
||||||
offerSent = false
|
offerSent = false
|
||||||
remoteDescriptionSet = false
|
remoteDescriptionSet = false
|
||||||
keyExchangeSent = false
|
keyExchangeSent = false
|
||||||
createRoomSent = false
|
activeSignalSent = false
|
||||||
lastPeerSharedPublicHex = ""
|
lastPeerSharedPublicHex = ""
|
||||||
lastRemoteOfferFingerprint = ""
|
lastRemoteOfferFingerprint = ""
|
||||||
lastLocalOfferFingerprint = ""
|
lastLocalOfferFingerprint = ""
|
||||||
@@ -1273,7 +1345,8 @@ object CallManager {
|
|||||||
append(" phase=").append(st.phase)
|
append(" phase=").append(st.phase)
|
||||||
append(" role=").append(role)
|
append(" role=").append(role)
|
||||||
append(" peer=").append(st.peerPublicKey.take(12))
|
append(" peer=").append(st.peerPublicKey.take(12))
|
||||||
append(" room=").append(roomId.take(16))
|
append(" callId=").append(serverCallId.take(16))
|
||||||
|
append(" join=").append(serverJoinToken.take(16))
|
||||||
append(" offerSent=").append(offerSent)
|
append(" offerSent=").append(offerSent)
|
||||||
append(" remoteDescSet=").append(remoteDescriptionSet)
|
append(" remoteDescSet=").append(remoteDescriptionSet)
|
||||||
append(" e2eeAvail=").append(e2eeAvailable)
|
append(" e2eeAvail=").append(e2eeAvailable)
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ enum class SignalType(val value: Int) {
|
|||||||
KEY_EXCHANGE(1),
|
KEY_EXCHANGE(1),
|
||||||
ACTIVE_CALL(2),
|
ACTIVE_CALL(2),
|
||||||
END_CALL(3),
|
END_CALL(3),
|
||||||
CREATE_ROOM(4),
|
ACTIVE(4),
|
||||||
END_CALL_BECAUSE_PEER_DISCONNECTED(5),
|
END_CALL_BECAUSE_PEER_DISCONNECTED(5),
|
||||||
END_CALL_BECAUSE_BUSY(6);
|
END_CALL_BECAUSE_BUSY(6),
|
||||||
|
ACCEPT(7),
|
||||||
|
RINGING_TIMEOUT(8);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromValue(value: Int): SignalType =
|
fun fromValue(value: Int): SignalType =
|
||||||
@@ -25,7 +27,8 @@ class PacketSignalPeer : Packet() {
|
|||||||
var dst: String = ""
|
var dst: String = ""
|
||||||
var sharedPublic: String = ""
|
var sharedPublic: String = ""
|
||||||
var signalType: SignalType = SignalType.CALL
|
var signalType: SignalType = SignalType.CALL
|
||||||
var roomId: String = ""
|
var callId: String = ""
|
||||||
|
var joinToken: String = ""
|
||||||
|
|
||||||
override fun getPacketId(): Int = 0x1A
|
override fun getPacketId(): Int = 0x1A
|
||||||
|
|
||||||
@@ -33,7 +36,8 @@ class PacketSignalPeer : Packet() {
|
|||||||
signalType = SignalType.fromValue(stream.readInt8())
|
signalType = SignalType.fromValue(stream.readInt8())
|
||||||
if (
|
if (
|
||||||
signalType == SignalType.END_CALL_BECAUSE_BUSY ||
|
signalType == SignalType.END_CALL_BECAUSE_BUSY ||
|
||||||
signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED
|
signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED ||
|
||||||
|
signalType == SignalType.RINGING_TIMEOUT
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -42,8 +46,13 @@ class PacketSignalPeer : Packet() {
|
|||||||
if (signalType == SignalType.KEY_EXCHANGE) {
|
if (signalType == SignalType.KEY_EXCHANGE) {
|
||||||
sharedPublic = stream.readString()
|
sharedPublic = stream.readString()
|
||||||
}
|
}
|
||||||
if (signalType == SignalType.CREATE_ROOM) {
|
if (
|
||||||
roomId = stream.readString()
|
signalType == SignalType.CALL ||
|
||||||
|
signalType == SignalType.ACCEPT ||
|
||||||
|
signalType == SignalType.END_CALL
|
||||||
|
) {
|
||||||
|
callId = stream.readString()
|
||||||
|
joinToken = stream.readString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +62,8 @@ class PacketSignalPeer : Packet() {
|
|||||||
stream.writeInt8(signalType.value)
|
stream.writeInt8(signalType.value)
|
||||||
if (
|
if (
|
||||||
signalType == SignalType.END_CALL_BECAUSE_BUSY ||
|
signalType == SignalType.END_CALL_BECAUSE_BUSY ||
|
||||||
signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED
|
signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED ||
|
||||||
|
signalType == SignalType.RINGING_TIMEOUT
|
||||||
) {
|
) {
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
@@ -62,8 +72,13 @@ class PacketSignalPeer : Packet() {
|
|||||||
if (signalType == SignalType.KEY_EXCHANGE) {
|
if (signalType == SignalType.KEY_EXCHANGE) {
|
||||||
stream.writeString(sharedPublic)
|
stream.writeString(sharedPublic)
|
||||||
}
|
}
|
||||||
if (signalType == SignalType.CREATE_ROOM) {
|
if (
|
||||||
stream.writeString(roomId)
|
signalType == SignalType.CALL ||
|
||||||
|
signalType == SignalType.ACCEPT ||
|
||||||
|
signalType == SignalType.END_CALL
|
||||||
|
) {
|
||||||
|
stream.writeString(callId)
|
||||||
|
stream.writeString(joinToken)
|
||||||
}
|
}
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,16 +19,12 @@ enum class WebRTCSignalType(val value: Int) {
|
|||||||
class PacketWebRTC : Packet() {
|
class PacketWebRTC : Packet() {
|
||||||
var signalType: WebRTCSignalType = WebRTCSignalType.OFFER
|
var signalType: WebRTCSignalType = WebRTCSignalType.OFFER
|
||||||
var sdpOrCandidate: String = ""
|
var sdpOrCandidate: String = ""
|
||||||
var publicKey: String = ""
|
|
||||||
var deviceId: String = ""
|
|
||||||
|
|
||||||
override fun getPacketId(): Int = 0x1B
|
override fun getPacketId(): Int = 0x1B
|
||||||
|
|
||||||
override fun receive(stream: Stream) {
|
override fun receive(stream: Stream) {
|
||||||
signalType = WebRTCSignalType.fromValue(stream.readInt8())
|
signalType = WebRTCSignalType.fromValue(stream.readInt8())
|
||||||
sdpOrCandidate = stream.readString()
|
sdpOrCandidate = stream.readString()
|
||||||
publicKey = stream.readString()
|
|
||||||
deviceId = stream.readString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(): Stream {
|
override fun send(): Stream {
|
||||||
@@ -36,8 +32,6 @@ class PacketWebRTC : Packet() {
|
|||||||
stream.writeInt16(getPacketId())
|
stream.writeInt16(getPacketId())
|
||||||
stream.writeInt8(signalType.value)
|
stream.writeInt8(signalType.value)
|
||||||
stream.writeString(sdpOrCandidate)
|
stream.writeString(sdpOrCandidate)
|
||||||
stream.writeString(publicKey)
|
|
||||||
stream.writeString(deviceId)
|
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,10 +135,9 @@ class Protocol(
|
|||||||
"status=${packet.status} ts=${packet.timestamp}"
|
"status=${packet.status} ts=${packet.timestamp}"
|
||||||
is PacketSignalPeer ->
|
is PacketSignalPeer ->
|
||||||
"type=${packet.signalType} src=${shortKey(packet.src)} dst=${shortKey(packet.dst)} " +
|
"type=${packet.signalType} src=${shortKey(packet.src)} dst=${shortKey(packet.dst)} " +
|
||||||
"sharedLen=${packet.sharedPublic.length} room=${shortKey(packet.roomId, 12)}"
|
"sharedLen=${packet.sharedPublic.length} callId=${shortKey(packet.callId, 12)} join=${shortKey(packet.joinToken, 12)}"
|
||||||
is PacketWebRTC ->
|
is PacketWebRTC ->
|
||||||
"type=${packet.signalType} sdpLen=${packet.sdpOrCandidate.length} " +
|
"type=${packet.signalType} sdpLen=${packet.sdpOrCandidate.length} " +
|
||||||
"pk=${shortKey(packet.publicKey)} device=${shortKey(packet.deviceId, 12)} " +
|
|
||||||
"preview='${shortText(packet.sdpOrCandidate, 64)}'"
|
"preview='${shortText(packet.sdpOrCandidate, 64)}'"
|
||||||
is PacketIceServers ->
|
is PacketIceServers ->
|
||||||
"count=${packet.iceServers.size} firstUrl='${packet.iceServers.firstOrNull()?.url?.let { shortText(it, 40) } ?: "<none>"}'"
|
"count=${packet.iceServers.size} firstUrl='${packet.iceServers.firstOrNull()?.url?.let { shortText(it, 40) } ?: "<none>"}'"
|
||||||
|
|||||||
@@ -1373,11 +1373,12 @@ object ProtocolManager {
|
|||||||
src: String = "",
|
src: String = "",
|
||||||
dst: String = "",
|
dst: String = "",
|
||||||
sharedPublic: String = "",
|
sharedPublic: String = "",
|
||||||
roomId: String = ""
|
callId: String = "",
|
||||||
|
joinToken: String = ""
|
||||||
) {
|
) {
|
||||||
addLog(
|
addLog(
|
||||||
"📡 CALL TX type=$signalType src=${shortKeyForLog(src)} dst=${shortKeyForLog(dst)} " +
|
"📡 CALL TX type=$signalType src=${shortKeyForLog(src)} dst=${shortKeyForLog(dst)} " +
|
||||||
"sharedLen=${sharedPublic.length} room=${shortKeyForLog(roomId, 12)}"
|
"sharedLen=${sharedPublic.length} callId=${shortKeyForLog(callId, 12)} join=${shortKeyForLog(joinToken, 12)}"
|
||||||
)
|
)
|
||||||
send(
|
send(
|
||||||
PacketSignalPeer().apply {
|
PacketSignalPeer().apply {
|
||||||
@@ -1385,7 +1386,8 @@ object ProtocolManager {
|
|||||||
this.src = src
|
this.src = src
|
||||||
this.dst = dst
|
this.dst = dst
|
||||||
this.sharedPublic = sharedPublic
|
this.sharedPublic = sharedPublic
|
||||||
this.roomId = roomId
|
this.callId = callId
|
||||||
|
this.joinToken = joinToken
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1394,19 +1396,14 @@ object ProtocolManager {
|
|||||||
* Send WebRTC signaling packet (0x1B).
|
* Send WebRTC signaling packet (0x1B).
|
||||||
*/
|
*/
|
||||||
fun sendWebRtcSignal(signalType: WebRTCSignalType, sdpOrCandidate: String) {
|
fun sendWebRtcSignal(signalType: WebRTCSignalType, sdpOrCandidate: String) {
|
||||||
val pk = try { getProtocol().getPublicKey().orEmpty() } catch (_: Exception) { "" }
|
|
||||||
val did = appContext?.let { getOrCreateDeviceId(it) } ?: ""
|
|
||||||
addLog(
|
addLog(
|
||||||
"📡 WEBRTC TX type=$signalType sdpLen=${sdpOrCandidate.length} " +
|
"📡 WEBRTC TX type=$signalType sdpLen=${sdpOrCandidate.length} " +
|
||||||
"pk=${shortKeyForLog(pk)} did=${shortKeyForLog(did, 12)} " +
|
|
||||||
"preview='${shortTextForLog(sdpOrCandidate, 56)}'"
|
"preview='${shortTextForLog(sdpOrCandidate, 56)}'"
|
||||||
)
|
)
|
||||||
send(
|
send(
|
||||||
PacketWebRTC().apply {
|
PacketWebRTC().apply {
|
||||||
this.signalType = signalType
|
this.signalType = signalType
|
||||||
this.sdpOrCandidate = sdpOrCandidate
|
this.sdpOrCandidate = sdpOrCandidate
|
||||||
this.publicKey = pk
|
|
||||||
this.deviceId = did
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1428,7 +1425,7 @@ object ProtocolManager {
|
|||||||
(packet as? PacketSignalPeer)?.let {
|
(packet as? PacketSignalPeer)?.let {
|
||||||
addLog(
|
addLog(
|
||||||
"📡 CALL RX type=${it.signalType} src=${shortKeyForLog(it.src)} dst=${shortKeyForLog(it.dst)} " +
|
"📡 CALL RX type=${it.signalType} src=${shortKeyForLog(it.src)} dst=${shortKeyForLog(it.dst)} " +
|
||||||
"sharedLen=${it.sharedPublic.length} room=${shortKeyForLog(it.roomId, 12)}"
|
"sharedLen=${it.sharedPublic.length} callId=${shortKeyForLog(it.callId, 12)} join=${shortKeyForLog(it.joinToken, 12)}"
|
||||||
)
|
)
|
||||||
callback(it)
|
callback(it)
|
||||||
}
|
}
|
||||||
@@ -1450,7 +1447,6 @@ object ProtocolManager {
|
|||||||
(packet as? PacketWebRTC)?.let {
|
(packet as? PacketWebRTC)?.let {
|
||||||
addLog(
|
addLog(
|
||||||
"📡 WEBRTC RX type=${it.signalType} sdpLen=${it.sdpOrCandidate.length} " +
|
"📡 WEBRTC RX type=${it.signalType} sdpLen=${it.sdpOrCandidate.length} " +
|
||||||
"pk=${shortKeyForLog(it.publicKey)} did=${shortKeyForLog(it.deviceId, 12)} " +
|
|
||||||
"preview='${shortTextForLog(it.sdpOrCandidate, 56)}'"
|
"preview='${shortTextForLog(it.sdpOrCandidate, 56)}'"
|
||||||
)
|
)
|
||||||
callback(it)
|
callback(it)
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
"public_key",
|
"public_key",
|
||||||
"publicKey"
|
"publicKey"
|
||||||
)
|
)
|
||||||
|
val callId = firstNonBlank(data, "callId", "call_id")
|
||||||
|
val joinToken = firstNonBlank(data, "joinToken", "join_token")
|
||||||
val senderName =
|
val senderName =
|
||||||
firstNonBlank(
|
firstNonBlank(
|
||||||
data,
|
data,
|
||||||
@@ -203,7 +205,8 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
handleIncomingCallPush(
|
handleIncomingCallPush(
|
||||||
dialogKey = dialogKey ?: senderPublicKey.orEmpty(),
|
dialogKey = dialogKey ?: senderPublicKey.orEmpty(),
|
||||||
title = senderName,
|
title = senderName,
|
||||||
body = messagePreview
|
callId = callId.orEmpty(),
|
||||||
|
joinToken = joinToken.orEmpty()
|
||||||
)
|
)
|
||||||
handledByData = true
|
handledByData = true
|
||||||
}
|
}
|
||||||
@@ -395,7 +398,12 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Супер push входящего звонка: пробуждаем протокол и запускаем ForegroundService с incoming call UI */
|
/** Супер push входящего звонка: пробуждаем протокол и запускаем ForegroundService с incoming call UI */
|
||||||
private fun handleIncomingCallPush(dialogKey: String, title: String, body: String) {
|
private fun handleIncomingCallPush(
|
||||||
|
dialogKey: String,
|
||||||
|
title: String,
|
||||||
|
callId: String,
|
||||||
|
joinToken: String
|
||||||
|
) {
|
||||||
pushCallLog("handleIncomingCallPush dialog=$dialogKey title=$title")
|
pushCallLog("handleIncomingCallPush dialog=$dialogKey title=$title")
|
||||||
wakeProtocolFromPush("call")
|
wakeProtocolFromPush("call")
|
||||||
|
|
||||||
@@ -415,7 +423,14 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val dedupKey = "call:${normalizedDialog.ifEmpty { "__no_dialog__" }}"
|
val normalizedCallId = callId.trim()
|
||||||
|
val normalizedJoinToken = joinToken.trim()
|
||||||
|
val dedupKey =
|
||||||
|
if (normalizedCallId.isNotBlank()) {
|
||||||
|
"call:$normalizedCallId"
|
||||||
|
} else {
|
||||||
|
"call:${normalizedDialog.ifEmpty { "__no_dialog__" }}"
|
||||||
|
}
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val lastTs = lastNotifTimestamps[dedupKey]
|
val lastTs = lastNotifTimestamps[dedupKey]
|
||||||
if (lastTs != null && now - lastTs < DEDUP_WINDOW_MS) {
|
if (lastTs != null && now - lastTs < DEDUP_WINDOW_MS) {
|
||||||
@@ -428,7 +443,12 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
pushCallLog("resolvedName=$resolvedName, calling setIncomingFromPush")
|
pushCallLog("resolvedName=$resolvedName, calling setIncomingFromPush")
|
||||||
|
|
||||||
// Сразу ставим CallManager в INCOMING — не ждём WebSocket
|
// Сразу ставим CallManager в INCOMING — не ждём WebSocket
|
||||||
CallManager.setIncomingFromPush(normalizedDialog, resolvedName)
|
CallManager.setIncomingFromPush(
|
||||||
|
peerPublicKey = normalizedDialog,
|
||||||
|
peerTitle = resolvedName,
|
||||||
|
callId = normalizedCallId,
|
||||||
|
joinToken = normalizedJoinToken
|
||||||
|
)
|
||||||
pushCallLog("setIncomingFromPush done, phase=${CallManager.state.value.phase}")
|
pushCallLog("setIncomingFromPush done, phase=${CallManager.state.value.phase}")
|
||||||
|
|
||||||
// Пробуем запустить IncomingCallActivity напрямую из FCM
|
// Пробуем запустить IncomingCallActivity напрямую из FCM
|
||||||
|
|||||||
Reference in New Issue
Block a user