Релиз 1.4.3: полноэкранные входящие звонки, аватарки в уведомлениях, фиксы
Some checks failed
Android Kernel Build / build (push) Failing after 4m6s
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:
164
app/src/main/java/com/rosetta/messenger/IncomingCallActivity.kt
Normal file
164
app/src/main/java/com/rosetta/messenger/IncomingCallActivity.kt
Normal 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user