Добавлен кастомный WebRTC AAR для CI + фиксы звонков
All checks were successful
Android Kernel Build / build (push) Successful in 18m55s

- libwebrtc-custom.aar закоммичен (был в .gitignore, CI использовал Maven без relative vtables → SIGSEGV)
- Фикс ForegroundServiceDidNotStartInTimeException (safeStopForeground)
- Фикс бесконечного "Exchanging keys" (ретрай KEY_EXCHANGE, auto-bind account)
- Фикс "Unknown" при сбросе звонка (stop ForegroundService до сброса state)
- Decline работает во всех фазах звонка
This commit is contained in:
2026-04-02 12:23:50 +05:00
parent c90136f563
commit 3217aeaeeb
4 changed files with 69 additions and 20 deletions

View File

@@ -53,15 +53,13 @@ class CallForegroundService : Service() {
when (action) {
ACTION_STOP -> {
notifLog("ACTION_STOP → stopSelf")
stopForegroundCompat()
stopSelf()
safeStopForeground()
return START_NOT_STICKY
}
ACTION_END -> {
notifLog("ACTION_END → endCall")
CallManager.endCall()
stopForegroundCompat()
stopSelf()
safeStopForeground()
return START_NOT_STICKY
}
ACTION_DECLINE -> {
@@ -70,11 +68,9 @@ class CallForegroundService : Service() {
if (phase == CallPhase.INCOMING) {
CallManager.declineIncomingCall()
} else {
// Если звонок уже не в INCOMING (CONNECTING/ACTIVE) — endCall
CallManager.endCall()
}
stopForegroundCompat()
stopSelf()
safeStopForeground()
return START_NOT_STICKY
}
ACTION_ACCEPT -> {
@@ -378,6 +374,26 @@ class CallForegroundService : Service() {
}
}
/** Безопасная остановка: startForeground → stopForeground → stopSelf.
* Предотвращает ForegroundServiceDidNotStartInTimeException. */
private fun safeStopForeground() {
ensureNotificationChannel()
try {
startForeground(NOTIFICATION_ID, buildPlaceholderNotification())
} catch (_: Throwable) {}
stopForegroundCompat()
stopSelf()
}
private fun buildPlaceholderNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Rosetta")
.setPriority(NotificationCompat.PRIORITY_LOW)
.setSilent(true)
.build()
}
companion object {
private const val TAG = "CallForegroundService"
private const val CHANNEL_ID = "rosetta_calls"
@@ -404,7 +420,10 @@ class CallForegroundService : Service() {
fun syncWithCallState(context: Context, state: CallUiState) {
val appContext = context.applicationContext
if (state.phase == CallPhase.IDLE) {
appContext.stopService(Intent(appContext, CallForegroundService::class.java))
// Используем ACTION_STOP вместо stopService — он вызовет safeStopForeground
val stopIntent = Intent(appContext, CallForegroundService::class.java).setAction(ACTION_STOP)
runCatching { appContext.startService(stopIntent) }
.onFailure { appContext.stopService(Intent(appContext, CallForegroundService::class.java)) }
return
}

View File

@@ -263,28 +263,57 @@ object CallManager {
val snapshot = _state.value
if (snapshot.phase != CallPhase.INCOMING) return CallActionResult.NOT_INCOMING
if (snapshot.peerPublicKey.isBlank()) return CallActionResult.INVALID_TARGET
if (ownPublicKey.isBlank()) return CallActionResult.ACCOUNT_NOT_BOUND
// Если ownPublicKey пустой (push разбудил но аккаунт не привязан) — попробуем привязать
if (ownPublicKey.isBlank()) {
val lastPk = appContext?.let { com.rosetta.messenger.data.AccountManager(it).getLastLoggedPublicKey() }.orEmpty()
if (lastPk.isNotBlank()) {
bindAccount(lastPk)
breadcrumb("acceptIncomingCall: auto-bind account pk=${lastPk.take(8)}")
} else {
return CallActionResult.ACCOUNT_NOT_BOUND
}
}
role = CallRole.CALLEE
generateSessionKeys()
val localPublic = localPublicKey ?: return CallActionResult.INVALID_TARGET
ProtocolManager.sendCallSignal(
signalType = SignalType.KEY_EXCHANGE,
src = ownPublicKey,
dst = snapshot.peerPublicKey,
sharedPublic = localPublic.toHex()
)
keyExchangeSent = true
incomingRingTimeoutJob?.cancel()
incomingRingTimeoutJob = null
updateState {
it.copy(
phase = CallPhase.CONNECTING,
statusText = "Exchanging keys..."
statusText = "Connecting..."
)
}
// Отправляем KEY_EXCHANGE — если WebSocket не подключен, ждём и ретраим
scope.launch {
var sent = false
for (attempt in 1..30) { // 30 * 200ms = 6 sec
if (ProtocolManager.isAuthenticated()) {
ProtocolManager.sendCallSignal(
signalType = SignalType.KEY_EXCHANGE,
src = ownPublicKey,
dst = snapshot.peerPublicKey,
sharedPublic = localPublic.toHex()
)
keyExchangeSent = true
breadcrumb("acceptIncomingCall: KEY_EXCHANGE sent (attempt #$attempt)")
sent = true
break
}
breadcrumb("acceptIncomingCall: waiting for auth (attempt #$attempt)")
kotlinx.coroutines.delay(200)
}
if (!sent) {
breadcrumb("acceptIncomingCall: FAILED to send KEY_EXCHANGE after 6s — resetting")
resetSession(reason = "Failed to connect", notifyPeer = false)
}
}
breadcrumbState("acceptIncomingCall")
return CallActionResult.STARTED
}
@@ -940,9 +969,9 @@ object CallManager {
incomingRingTimeoutJob?.cancel()
incomingRingTimeoutJob = null
setSpeakerphone(false)
_state.value = CallUiState()
// Останавливаем ForegroundService
// Останавливаем ForegroundService ДО сброса state — иначе "Unknown" мелькает
appContext?.let { CallForegroundService.stop(it) }
_state.value = CallUiState()
}
private fun resetRtcObjects() {