diff --git a/.gitignore b/.gitignore index 8510b9c..9009561 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Built application files *.apk -*.aar +# *.aar — кастомный WebRTC разрешён в app/libs/ +!app/libs/*.aar *.ap_ *.aab diff --git a/app/libs/libwebrtc-custom.aar b/app/libs/libwebrtc-custom.aar new file mode 100644 index 0000000..e5631e0 Binary files /dev/null and b/app/libs/libwebrtc-custom.aar differ diff --git a/app/src/main/java/com/rosetta/messenger/network/CallForegroundService.kt b/app/src/main/java/com/rosetta/messenger/network/CallForegroundService.kt index 8a89d2d..eb2d0f7 100644 --- a/app/src/main/java/com/rosetta/messenger/network/CallForegroundService.kt +++ b/app/src/main/java/com/rosetta/messenger/network/CallForegroundService.kt @@ -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 } diff --git a/app/src/main/java/com/rosetta/messenger/network/CallManager.kt b/app/src/main/java/com/rosetta/messenger/network/CallManager.kt index 5c43352..f7bafab 100644 --- a/app/src/main/java/com/rosetta/messenger/network/CallManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/CallManager.kt @@ -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() {