Релиз 1.4.3: полноэкранные входящие звонки, аватарки в уведомлениях, фиксы
Some checks failed
Android Kernel Build / build (push) Failing after 4m6s

Звонки:
- IncomingCallActivity — полноэкранный UI входящего звонка поверх lock screen
- fullScreenIntent на нотификации для Android 12+
- ForegroundService синхронизируется при смене фазы и имени
- Запрос fullScreenIntent permission на Android 14+
- dispose() PeerConnection при завершении звонка
- Защита от CREATE_ROOM без ключей (звонок на другом устройстве)
- Дедупликация push + WebSocket сигналов
- setIncomingFromPush — CallManager сразу в INCOMING по push
- Accept ждёт до 5 сек если WebSocket не доставил сигнал
- Decline работает во всех фазах (не только INCOMING)
- Баннер активного звонка внутри диалога

Уведомления:
- Аватарки и имена по publicKey в уведомлениях (message + call)
- Настройка "Avatars in Notifications" в разделе Notifications

UI:
- Ограничение fontScale до 1.3x (вёрстка не ломается на огромном тексте)
- Новые обои: Light 1-3 для светлой темы, убраны старые back_*
- ContentScale.Crop для превью обоев (без растяжения)

CI/CD:
- NDK/CMake в CI, local.properties, ANDROID_NDK_HOME
- Ограничение JVM heap для CI раннера

Диагностика:
- Логирование call notification flow в crash_reports (rosettadev1)
- FCM токен в crash_reports
This commit is contained in:
2026-04-02 01:18:20 +05:00
parent 803fda9abe
commit 876c1ab4df
30 changed files with 789 additions and 170 deletions

View File

@@ -0,0 +1,164 @@
package com.rosetta.messenger
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.*
import com.rosetta.messenger.network.CallActionResult
import com.rosetta.messenger.network.CallForegroundService
import com.rosetta.messenger.network.CallManager
import com.rosetta.messenger.network.CallPhase
import com.rosetta.messenger.ui.chats.calls.CallOverlay
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
/**
* Лёгкая Activity для показа входящего звонка на lock screen.
* Показывается поверх экрана блокировки, без auth/splash.
* При Accept → переходит в MainActivity. При Decline → закрывается.
*/
class IncomingCallActivity : ComponentActivity() {
companion object {
private const val TAG = "IncomingCallActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
try {
super.onCreate(savedInstanceState)
} catch (e: Throwable) {
Log.e(TAG, "super.onCreate CRASHED", e)
callLog("super.onCreate CRASHED: ${e.message}")
finish()
return
}
callLog("onCreate START")
// Показываем поверх lock screen и включаем экран
callLog("setting lock screen flags, SDK=${Build.VERSION.SDK_INT}")
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
} else {
@Suppress("DEPRECATION")
window.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
)
}
// Dismiss keyguard
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val km = getSystemService(Context.KEYGUARD_SERVICE) as? KeyguardManager
km?.requestDismissKeyguard(this, null)
} else {
@Suppress("DEPRECATION")
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
}
try {
CallManager.initialize(applicationContext)
callLog("CallManager initialized, phase=${CallManager.state.value.phase}")
} catch (e: Throwable) {
callLog("CallManager.initialize CRASHED: ${e.message}")
Log.e(TAG, "CallManager init failed", e)
}
callLog("calling setContent")
setContent {
val callState by CallManager.state.collectAsState()
// Ждём до 10 сек пока WebSocket доставит сигнал (CallManager перейдёт из IDLE)
var wasIncoming by remember { mutableStateOf(false) }
LaunchedEffect(callState.phase) {
callLog("phase changed: ${callState.phase}")
if (callState.phase == CallPhase.INCOMING) wasIncoming = true
// Закрываем только если звонок реально начался и потом завершился
if (callState.phase == CallPhase.IDLE && wasIncoming) {
callLog("IDLE after INCOMING → finish()")
finish()
} else if (callState.phase == CallPhase.CONNECTING ||
callState.phase == CallPhase.ACTIVE) {
callLog("${callState.phase} → openMainActivity + finish")
openMainActivity()
finish()
}
}
// Показываем INCOMING даже если CallManager ещё в IDLE (push раньше WebSocket)
val displayState = if (callState.phase == CallPhase.IDLE) {
callState.copy(phase = CallPhase.INCOMING, statusText = "Incoming call...")
} else callState
RosettaAndroidTheme(darkTheme = true) {
CallOverlay(
state = displayState,
isDarkTheme = true,
isExpanded = true,
onAccept = {
callLog("onAccept tapped, phase=${callState.phase}")
if (callState.phase == CallPhase.INCOMING) {
val result = CallManager.acceptIncomingCall()
callLog("acceptIncomingCall result=$result")
if (result == CallActionResult.STARTED) {
openMainActivity()
finish()
}
} else {
callLog("onAccept: phase not INCOMING yet, waiting...")
// WebSocket ещё не доставил CALL — открываем MainActivity,
// она подождёт и примет звонок
openMainActivity()
finish()
}
},
onDecline = {
callLog("onDecline tapped")
CallManager.declineIncomingCall()
finish()
},
onEnd = {
callLog("onEnd tapped")
CallManager.endCall()
finish()
},
onToggleMute = { CallManager.toggleMute() },
onToggleSpeaker = { CallManager.toggleSpeaker() }
)
}
}
}
private fun openMainActivity() {
callLog("openMainActivity")
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(CallForegroundService.EXTRA_OPEN_CALL_FROM_NOTIFICATION, true)
}
startActivity(intent)
}
private fun callLog(msg: String) {
Log.d(TAG, msg)
try {
val ctx = applicationContext ?: return
val dir = java.io.File(ctx.filesDir, "crash_reports")
if (!dir.exists()) dir.mkdirs()
val f = java.io.File(dir, "call_notification_log.txt")
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
f.appendText("$ts [IncomingCallActivity] $msg\n")
} catch (e: Throwable) {
Log.e(TAG, "callLog write failed: ${e.message}")
}
}
}