Реализованы звонки в диалоге и полный permission flow Android

This commit is contained in:
2026-03-23 10:56:52 +05:00
parent 4664aa9482
commit 9778e3b196
6 changed files with 1174 additions and 2 deletions

View File

@@ -4,6 +4,7 @@ import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -32,6 +33,8 @@ import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.data.RecentSearchesManager
import com.rosetta.messenger.data.resolveAccountDisplayName
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.CallActionResult
import com.rosetta.messenger.network.CallManager
import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.network.ProtocolState
import com.rosetta.messenger.network.SearchUser
@@ -46,6 +49,7 @@ import com.rosetta.messenger.ui.chats.GroupInfoScreen
import com.rosetta.messenger.ui.chats.GroupSetupScreen
import com.rosetta.messenger.ui.chats.RequestsListScreen
import com.rosetta.messenger.ui.chats.SearchScreen
import com.rosetta.messenger.ui.chats.calls.CallOverlay
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
import com.rosetta.messenger.ui.components.SwipeBackBackgroundEffect
import com.rosetta.messenger.ui.components.SwipeBackContainer
@@ -116,6 +120,7 @@ class MainActivity : FragmentActivity() {
// 🔥 Инициализируем ProtocolManager для обработки онлайн статусов
ProtocolManager.initialize(this)
CallManager.initialize(this)
// 🔔 Инициализируем Firebase для push-уведомлений
initializeFirebase()
@@ -581,6 +586,177 @@ fun MainScreen(
// Load username AND name from AccountManager (persisted in DataStore)
val context = LocalContext.current
val callScope = rememberCoroutineScope()
val callUiState by CallManager.state.collectAsState()
var pendingOutgoingCall by remember { mutableStateOf<SearchUser?>(null) }
var pendingIncomingAccept by remember { mutableStateOf(false) }
var callPermissionsRequestedOnce by remember { mutableStateOf(false) }
val mandatoryCallPermissions = remember {
listOf(Manifest.permission.RECORD_AUDIO)
}
val optionalCallPermissions = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
listOf(Manifest.permission.BLUETOOTH_CONNECT)
} else {
emptyList()
}
}
val permissionsToRequest = remember(mandatoryCallPermissions, optionalCallPermissions) {
mandatoryCallPermissions + optionalCallPermissions
}
val hasMandatoryCallPermissions: () -> Boolean =
remember(context, mandatoryCallPermissions) {
{
mandatoryCallPermissions.all { permission ->
ContextCompat.checkSelfPermission(context, permission) ==
PackageManager.PERMISSION_GRANTED
}
}
}
val hasOptionalCallPermissions: () -> Boolean =
remember(context, optionalCallPermissions) {
{
optionalCallPermissions.all { permission ->
ContextCompat.checkSelfPermission(context, permission) ==
PackageManager.PERMISSION_GRANTED
}
}
}
val showCallError: (CallActionResult) -> Unit = { result ->
val message =
when (result) {
CallActionResult.STARTED -> ""
CallActionResult.ALREADY_IN_CALL -> "Сначала заверши текущий звонок"
CallActionResult.NOT_AUTHENTICATED -> "Нет подключения к серверу"
CallActionResult.ACCOUNT_NOT_BOUND -> "Аккаунт еще не инициализирован"
CallActionResult.INVALID_TARGET -> "Не удалось определить пользователя для звонка"
CallActionResult.NOT_INCOMING -> "Входящий звонок не найден"
}
if (message.isNotBlank()) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
val resolveCallableUser: suspend (SearchUser) -> SearchUser? = resolve@{ user ->
val publicKey = user.publicKey.trim()
if (publicKey.isNotBlank()) {
return@resolve user.copy(publicKey = publicKey)
}
val usernameQuery = user.username.trim().trimStart('@')
if (usernameQuery.isBlank()) {
return@resolve null
}
ProtocolManager.getCachedUserByUsername(usernameQuery)?.let { cached ->
if (cached.publicKey.isNotBlank()) return@resolve cached
}
val results = ProtocolManager.searchUsers(usernameQuery)
results.firstOrNull {
it.publicKey.isNotBlank() &&
it.username.trim().trimStart('@')
.equals(usernameQuery, ignoreCase = true)
}?.let { return@resolve it }
return@resolve results.firstOrNull { it.publicKey.isNotBlank() }
}
val startOutgoingCallSafely: (SearchUser) -> Unit = { user ->
callScope.launch {
val resolved = resolveCallableUser(user)
if (resolved == null) {
showCallError(CallActionResult.INVALID_TARGET)
return@launch
}
val result = CallManager.startOutgoingCall(resolved)
if (result != CallActionResult.STARTED) {
showCallError(result)
}
}
}
val acceptIncomingCallSafely: () -> Unit = {
val result = CallManager.acceptIncomingCall()
if (result != CallActionResult.STARTED) {
showCallError(result)
}
}
val callPermissionLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions()
) { grantedMap ->
callPermissionsRequestedOnce = true
val micGranted =
grantedMap[Manifest.permission.RECORD_AUDIO] == true ||
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
val bluetoothGranted =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
true
} else {
grantedMap[Manifest.permission.BLUETOOTH_CONNECT] == true ||
ContextCompat.checkSelfPermission(
context,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
}
if (!micGranted) {
Toast.makeText(
context,
"Для звонков нужен доступ к микрофону",
Toast.LENGTH_SHORT
).show()
} else {
pendingOutgoingCall?.let { startOutgoingCallSafely(it) }
if (pendingIncomingAccept) {
acceptIncomingCallSafely()
}
if (!bluetoothGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Toast.makeText(
context,
"Bluetooth недоступен: гарнитура может не работать",
Toast.LENGTH_SHORT
).show()
}
}
pendingOutgoingCall = null
pendingIncomingAccept = false
}
val startCallWithPermission: (SearchUser) -> Unit = { user ->
val shouldRequestPermissions =
!hasMandatoryCallPermissions() ||
(!callPermissionsRequestedOnce && !hasOptionalCallPermissions())
if (!shouldRequestPermissions) {
startOutgoingCallSafely(user)
} else {
pendingOutgoingCall = user
callPermissionLauncher.launch(permissionsToRequest.toTypedArray())
}
}
val acceptCallWithPermission: () -> Unit = {
val shouldRequestPermissions =
!hasMandatoryCallPermissions() ||
(!callPermissionsRequestedOnce && !hasOptionalCallPermissions())
if (!shouldRequestPermissions) {
acceptIncomingCallSafely()
} else {
pendingIncomingAccept = true
callPermissionLauncher.launch(permissionsToRequest.toTypedArray())
}
}
LaunchedEffect(accountPublicKey) {
CallManager.bindAccount(accountPublicKey)
}
LaunchedEffect(accountPublicKey, reloadTrigger) {
if (accountPublicKey.isNotBlank()) {
val accountManager = AccountManager(context)
@@ -1075,6 +1251,9 @@ fun MainScreen(
currentUserUsername = accountUsername,
totalUnreadFromOthers = totalUnreadFromOthers,
onBack = { popChatAndChildren() },
onCallClick = { callableUser ->
startCallWithPermission(callableUser)
},
onUserProfileClick = { user ->
if (isCurrentAccountUser(user)) {
// Свой профиль из чата открываем поверх текущего чата,
@@ -1369,5 +1548,15 @@ fun MainScreen(
}
)
}
CallOverlay(
state = callUiState,
isDarkTheme = isDarkTheme,
onAccept = { acceptCallWithPermission() },
onDecline = { CallManager.declineIncomingCall() },
onEnd = { CallManager.endCall() },
onToggleMute = { CallManager.toggleMute() },
onToggleSpeaker = { CallManager.toggleSpeaker() }
)
}
}