Добавлен кастомный 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

3
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Built application files # Built application files
*.apk *.apk
*.aar # *.aar — кастомный WebRTC разрешён в app/libs/
!app/libs/*.aar
*.ap_ *.ap_
*.aab *.aab

Binary file not shown.

View File

@@ -53,15 +53,13 @@ class CallForegroundService : Service() {
when (action) { when (action) {
ACTION_STOP -> { ACTION_STOP -> {
notifLog("ACTION_STOP → stopSelf") notifLog("ACTION_STOP → stopSelf")
stopForegroundCompat() safeStopForeground()
stopSelf()
return START_NOT_STICKY return START_NOT_STICKY
} }
ACTION_END -> { ACTION_END -> {
notifLog("ACTION_END → endCall") notifLog("ACTION_END → endCall")
CallManager.endCall() CallManager.endCall()
stopForegroundCompat() safeStopForeground()
stopSelf()
return START_NOT_STICKY return START_NOT_STICKY
} }
ACTION_DECLINE -> { ACTION_DECLINE -> {
@@ -70,11 +68,9 @@ class CallForegroundService : Service() {
if (phase == CallPhase.INCOMING) { if (phase == CallPhase.INCOMING) {
CallManager.declineIncomingCall() CallManager.declineIncomingCall()
} else { } else {
// Если звонок уже не в INCOMING (CONNECTING/ACTIVE) — endCall
CallManager.endCall() CallManager.endCall()
} }
stopForegroundCompat() safeStopForeground()
stopSelf()
return START_NOT_STICKY return START_NOT_STICKY
} }
ACTION_ACCEPT -> { 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 { companion object {
private const val TAG = "CallForegroundService" private const val TAG = "CallForegroundService"
private const val CHANNEL_ID = "rosetta_calls" private const val CHANNEL_ID = "rosetta_calls"
@@ -404,7 +420,10 @@ class CallForegroundService : Service() {
fun syncWithCallState(context: Context, state: CallUiState) { fun syncWithCallState(context: Context, state: CallUiState) {
val appContext = context.applicationContext val appContext = context.applicationContext
if (state.phase == CallPhase.IDLE) { 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 return
} }

View File

@@ -263,28 +263,57 @@ object CallManager {
val snapshot = _state.value val snapshot = _state.value
if (snapshot.phase != CallPhase.INCOMING) return CallActionResult.NOT_INCOMING if (snapshot.phase != CallPhase.INCOMING) return CallActionResult.NOT_INCOMING
if (snapshot.peerPublicKey.isBlank()) return CallActionResult.INVALID_TARGET 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 role = CallRole.CALLEE
generateSessionKeys() generateSessionKeys()
val localPublic = localPublicKey ?: return CallActionResult.INVALID_TARGET 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?.cancel()
incomingRingTimeoutJob = null incomingRingTimeoutJob = null
updateState { updateState {
it.copy( it.copy(
phase = CallPhase.CONNECTING, 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") breadcrumbState("acceptIncomingCall")
return CallActionResult.STARTED return CallActionResult.STARTED
} }
@@ -940,9 +969,9 @@ object CallManager {
incomingRingTimeoutJob?.cancel() incomingRingTimeoutJob?.cancel()
incomingRingTimeoutJob = null incomingRingTimeoutJob = null
setSpeakerphone(false) setSpeakerphone(false)
_state.value = CallUiState() // Останавливаем ForegroundService ДО сброса state — иначе "Unknown" мелькает
// Останавливаем ForegroundService
appContext?.let { CallForegroundService.stop(it) } appContext?.let { CallForegroundService.stop(it) }
_state.value = CallUiState()
} }
private fun resetRtcObjects() { private fun resetRtcObjects() {