Реализованы звонки в диалоге и полный permission flow Android
This commit is contained in:
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user