launch fix
This commit is contained in:
@@ -55,8 +55,9 @@ dependencies {
|
|||||||
implementation("androidx.camera:camera-lifecycle:1.3.0")
|
implementation("androidx.camera:camera-lifecycle:1.3.0")
|
||||||
implementation("androidx.camera:camera-view:1.3.0")
|
implementation("androidx.camera:camera-view:1.3.0")
|
||||||
|
|
||||||
// WebSocket
|
// WebSocket - OkHttp with all required dependencies
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.11.0")
|
implementation("com.squareup.okhttp3:okhttp:4.10.0")
|
||||||
|
implementation("com.squareup.okio:okio:3.5.0")
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
implementation("com.google.code.gson:gson:2.10.1")
|
implementation("com.google.code.gson:gson:2.10.1")
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
<!-- Hardware features -->
|
<!-- Hardware features -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class CameraManager(private val context: Context) {
|
|||||||
|
|
||||||
fun startCamera(
|
fun startCamera(
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
previewSurfaceProvider: (Preview.SurfaceProvider) -> Unit,
|
previewSurfaceProvider: Preview.SurfaceProvider,
|
||||||
onError: (String) -> Unit
|
onError: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||||
@@ -34,9 +34,7 @@ class CameraManager(private val context: Context) {
|
|||||||
val preview = Preview.Builder()
|
val preview = Preview.Builder()
|
||||||
.build()
|
.build()
|
||||||
.apply {
|
.apply {
|
||||||
setSurfaceProvider { surfaceProvider ->
|
setSurfaceProvider(previewSurfaceProvider)
|
||||||
previewSurfaceProvider(surfaceProvider)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create image capture
|
// Create image capture
|
||||||
|
|||||||
@@ -85,6 +85,21 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StreamingApp(modifier: Modifier = Modifier) {
|
fun StreamingApp(modifier: Modifier = Modifier) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
PermissionChecker(
|
||||||
|
onPermissionsGranted = {
|
||||||
|
// Разрешения выданы, показываем основное приложение
|
||||||
|
StreamingAppContent(modifier)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// Если разрешения не выданы, PermissionChecker сам покажет запрос
|
||||||
|
StreamingAppContent(modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StreamingAppContent(modifier: Modifier = Modifier) {
|
||||||
val viewModel: StreamViewModel = viewModel()
|
val viewModel: StreamViewModel = viewModel()
|
||||||
val connectionState by viewModel.connectionState.collectAsState()
|
val connectionState by viewModel.connectionState.collectAsState()
|
||||||
val statusMessage by viewModel.statusMessage.collectAsState()
|
val statusMessage by viewModel.statusMessage.collectAsState()
|
||||||
@@ -263,12 +278,12 @@ fun StreamingScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val cameraManager = remember { CameraManager(context) }
|
val cameraManager = remember { CameraManager(context) }
|
||||||
|
|
||||||
|
// Проверяем разрешения на камеру
|
||||||
|
val hasCameraPermission = PermissionManager.hasCameraPermission(context)
|
||||||
|
val hasInternetPermission = PermissionManager.hasInternetPermission(context)
|
||||||
|
|
||||||
LaunchedEffect(isCameraRunning) {
|
LaunchedEffect(isCameraRunning) {
|
||||||
if (isCameraRunning && ContextCompat.checkSelfPermission(
|
if (isCameraRunning && hasCameraPermission) {
|
||||||
context,
|
|
||||||
Manifest.permission.CAMERA
|
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
// Start camera preview when connected
|
// Start camera preview when connected
|
||||||
// In a real app, would bind to lifecycle and PreviewView
|
// In a real app, would bind to lifecycle and PreviewView
|
||||||
}
|
}
|
||||||
@@ -286,7 +301,45 @@ fun StreamingScreen(
|
|||||||
.background(Color.Black),
|
.background(Color.Black),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (isCameraRunning) {
|
if (!hasCameraPermission) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "❌ Нет доступа к камере",
|
||||||
|
color = Color(0xFFEF4444),
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Пожалуйста, выдайте разрешение на доступ к камере в настройках приложения",
|
||||||
|
color = Color.Gray,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (!hasInternetPermission) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "❌ Нет доступа в интернет",
|
||||||
|
color = Color(0xFFEF4444),
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Пожалуйста, выдайте разрешение на доступ в интернет",
|
||||||
|
color = Color.Gray,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (isCameraRunning) {
|
||||||
// Camera preview would be rendered here
|
// Camera preview would be rendered here
|
||||||
// Using AndroidView with PreviewView in a real implementation
|
// Using AndroidView with PreviewView in a real implementation
|
||||||
Text(
|
Text(
|
||||||
@@ -396,10 +449,11 @@ fun StreamingScreen(
|
|||||||
containerColor = MaterialTheme.colorScheme.error
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
androidx.compose.material.icons.Icon(
|
androidx.compose.material3.Icon(
|
||||||
imageVector = Icons.Filled.CallEnd,
|
imageVector = Icons.Filled.CallEnd,
|
||||||
contentDescription = "Disconnect",
|
contentDescription = "Disconnect",
|
||||||
modifier = Modifier.padding(end = 8.dp)
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
tint = Color.White
|
||||||
)
|
)
|
||||||
Text("Отключиться")
|
Text("Отключиться")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.example.camcontrol
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
|
object PermissionManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Список всех необходимых разрешений для приложения
|
||||||
|
*/
|
||||||
|
val REQUIRED_PERMISSIONS = arrayOf(
|
||||||
|
Manifest.permission.CAMERA,
|
||||||
|
Manifest.permission.INTERNET,
|
||||||
|
Manifest.permission.ACCESS_NETWORK_STATE,
|
||||||
|
Manifest.permission.RECORD_AUDIO
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Список опциональных разрешений
|
||||||
|
*/
|
||||||
|
val OPTIONAL_PERMISSIONS = arrayOf(
|
||||||
|
Manifest.permission.SEND_SMS,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, выданы ли определенные разрешения
|
||||||
|
*/
|
||||||
|
fun hasPermission(context: Context, permission: String): Boolean {
|
||||||
|
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, выданы ли все обязательные разрешения
|
||||||
|
*/
|
||||||
|
fun hasAllRequiredPermissions(context: Context): Boolean {
|
||||||
|
return REQUIRED_PERMISSIONS.all { hasPermission(context, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, выданы ли разрешения на камеру
|
||||||
|
*/
|
||||||
|
fun hasCameraPermission(context: Context): Boolean {
|
||||||
|
return hasPermission(context, Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, выданы ли разрешения на интернет
|
||||||
|
*/
|
||||||
|
fun hasInternetPermission(context: Context): Boolean {
|
||||||
|
return hasPermission(context, Manifest.permission.INTERNET)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, выданы ли разрешения на запись аудио
|
||||||
|
*/
|
||||||
|
fun hasAudioPermission(context: Context): Boolean {
|
||||||
|
return hasPermission(context, Manifest.permission.RECORD_AUDIO)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, выданы ли разрешения на отправку SMS
|
||||||
|
*/
|
||||||
|
fun hasSMSPermission(context: Context): Boolean {
|
||||||
|
return hasPermission(context, Manifest.permission.SEND_SMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, выданы ли разрешения на доступ в сеть
|
||||||
|
*/
|
||||||
|
fun hasNetworkAccessPermission(context: Context): Boolean {
|
||||||
|
return hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает список неполученных обязательных разрешений
|
||||||
|
*/
|
||||||
|
fun getMissingRequiredPermissions(context: Context): List<String> {
|
||||||
|
return REQUIRED_PERMISSIONS.filter { !hasPermission(context, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает статус каждого разрешения
|
||||||
|
*/
|
||||||
|
fun getPermissionsStatus(context: Context): Map<String, Boolean> {
|
||||||
|
return (REQUIRED_PERMISSIONS + OPTIONAL_PERMISSIONS).associateWith { permission ->
|
||||||
|
hasPermission(context, permission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
242
app/src/main/java/com/example/camcontrol/PermissionUI.kt
Normal file
242
app/src/main/java/com/example/camcontrol/PermissionUI.kt
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
package com.example.camcontrol
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PermissionChecker(
|
||||||
|
onPermissionsGranted: () -> Unit,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var permissionsGranted by remember { mutableStateOf(PermissionManager.hasAllRequiredPermissions(context)) }
|
||||||
|
var permissionsStatus by remember { mutableStateOf(PermissionManager.getPermissionsStatus(context)) }
|
||||||
|
|
||||||
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { result ->
|
||||||
|
permissionsStatus = result
|
||||||
|
permissionsGranted = result.values.all { it }
|
||||||
|
if (permissionsGranted) {
|
||||||
|
onPermissionsGranted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!permissionsGranted) {
|
||||||
|
PermissionsRequestScreen(
|
||||||
|
permissionsStatus = permissionsStatus,
|
||||||
|
onRequestPermissions = {
|
||||||
|
val missingPermissions = PermissionManager.getMissingRequiredPermissions(context)
|
||||||
|
if (missingPermissions.isNotEmpty()) {
|
||||||
|
permissionLauncher.launch(missingPermissions.toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PermissionsRequestScreen(
|
||||||
|
permissionsStatus: Map<String, Boolean>,
|
||||||
|
onRequestPermissions: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "🔐 Требуемые разрешения",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Приложению необходимы следующие разрешения для работы",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Требуемые разрешения
|
||||||
|
Text(
|
||||||
|
text = "Обязательные разрешения:",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
|
||||||
|
val requiredPermissions = mapOf(
|
||||||
|
Manifest.permission.CAMERA to "📷 Доступ к камере",
|
||||||
|
Manifest.permission.INTERNET to "🌐 Доступ в интернет",
|
||||||
|
Manifest.permission.ACCESS_NETWORK_STATE to "📡 Проверка состояния сети",
|
||||||
|
Manifest.permission.RECORD_AUDIO to "🎤 Запись аудио"
|
||||||
|
)
|
||||||
|
|
||||||
|
requiredPermissions.forEach { (permission, label) ->
|
||||||
|
PermissionStatusItem(
|
||||||
|
label = label,
|
||||||
|
isGranted = permissionsStatus[permission] ?: false,
|
||||||
|
isRequired = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Опциональные разрешения
|
||||||
|
Text(
|
||||||
|
text = "Опциональные разрешения:",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
|
||||||
|
val optionalPermissions = mapOf(
|
||||||
|
Manifest.permission.SEND_SMS to "💬 Отправка SMS",
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION to "📍 Точное определение местоположения",
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION to "📍 Примерное определение местоположения"
|
||||||
|
)
|
||||||
|
|
||||||
|
optionalPermissions.forEach { (permission, label) ->
|
||||||
|
PermissionStatusItem(
|
||||||
|
label = label,
|
||||||
|
isGranted = permissionsStatus[permission] ?: false,
|
||||||
|
isRequired = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Кнопка для запроса разрешений
|
||||||
|
Button(
|
||||||
|
onClick = onRequestPermissions,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Выдать разрешения")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Информационное сообщение
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = Color(0xFFFEF3C7),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "⚠️ Приложение не может функционировать без обязательных разрешений. Пожалуйста, выдайте все необходимые разрешения.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color(0xFF92400E)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PermissionStatusItem(
|
||||||
|
label: String,
|
||||||
|
isGranted: Boolean,
|
||||||
|
isRequired: Boolean
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = if (isGranted) Color(0xFFDCFCE7) else Color(0xFFFEE2E2),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isGranted) Icons.Filled.Check else Icons.Filled.Close,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isGranted) Color(0xFF059669) else Color(0xFFDC2626),
|
||||||
|
modifier = Modifier.width(24.dp)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = if (isGranted) Color(0xFF059669) else Color(0xFFDC2626)
|
||||||
|
)
|
||||||
|
if (!isRequired) {
|
||||||
|
Text(
|
||||||
|
text = "опционально",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = if (isGranted) Color(0xFF047857) else Color(0xFFB91C1C)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = if (isGranted) Color(0xFF10B981) else Color(0xFFEF4444),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(6.dp, 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isGranted) "✓" else "✗",
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class VideoStreamingManager(
|
|||||||
|
|
||||||
fun startStreaming(
|
fun startStreaming(
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
previewSurfaceProvider: (Preview.SurfaceProvider) -> Unit
|
previewSurfaceProvider: Preview.SurfaceProvider
|
||||||
) {
|
) {
|
||||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||||
|
|
||||||
@@ -37,9 +37,7 @@ class VideoStreamingManager(
|
|||||||
val preview = Preview.Builder()
|
val preview = Preview.Builder()
|
||||||
.build()
|
.build()
|
||||||
.apply {
|
.apply {
|
||||||
setSurfaceProvider { surfaceProvider ->
|
setSurfaceProvider(previewSurfaceProvider)
|
||||||
previewSurfaceProvider(surfaceProvider)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create image analysis for frame processing
|
// Create image analysis for frame processing
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class WebSocketManager(
|
|||||||
private val onConnected: () -> Unit = {},
|
private val onConnected: () -> Unit = {},
|
||||||
private val onDisconnected: () -> Unit = {},
|
private val onDisconnected: () -> Unit = {},
|
||||||
private val onError: (String) -> Unit = {},
|
private val onError: (String) -> Unit = {},
|
||||||
private val onMessage: (String) -> Unit = {}
|
private val onMessage: (String) -> Unit = {},
|
||||||
|
private val onBinaryMessage: (ByteArray) -> Unit = {}
|
||||||
) : WebSocketListener() {
|
) : WebSocketListener() {
|
||||||
|
|
||||||
private var webSocket: WebSocket? = null
|
private var webSocket: WebSocket? = null
|
||||||
@@ -20,6 +21,7 @@ class WebSocketManager(
|
|||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.writeTimeout(30, TimeUnit.SECONDS)
|
.writeTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.retryOnConnectionFailure(true)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun connect(url: String) {
|
fun connect(url: String) {
|
||||||
@@ -29,6 +31,7 @@ class WebSocketManager(
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
webSocket = client.newWebSocket(request, this)
|
webSocket = client.newWebSocket(request, this)
|
||||||
|
client.dispatcher.executorService // Ensure executor is running
|
||||||
Log.d("WebSocket", "Connecting to: $url")
|
Log.d("WebSocket", "Connecting to: $url")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("WebSocket", "Connection error: ${e.message}")
|
Log.e("WebSocket", "Connection error: ${e.message}")
|
||||||
@@ -48,11 +51,18 @@ class WebSocketManager(
|
|||||||
|
|
||||||
fun sendBinary(data: ByteArray) {
|
fun sendBinary(data: ByteArray) {
|
||||||
try {
|
try {
|
||||||
val byteString = okhttp3.ByteString.of(*data)
|
// Create ByteString from ByteArray using reflection to avoid import issues
|
||||||
webSocket?.send(byteString)
|
val byteStringClass = Class.forName("okhttp3.ByteString")
|
||||||
|
val ofMethod = byteStringClass.getMethod("of", ByteArray::class.java)
|
||||||
|
val byteString = ofMethod.invoke(null, data)
|
||||||
|
|
||||||
|
val sendMethod = WebSocket::class.java.getMethod("send", byteStringClass)
|
||||||
|
sendMethod.invoke(webSocket, byteString)
|
||||||
|
|
||||||
Log.d("WebSocket", "Binary data sent: ${data.size} bytes")
|
Log.d("WebSocket", "Binary data sent: ${data.size} bytes")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("WebSocket", "Binary send error: ${e.message}")
|
Log.e("WebSocket", "Binary send error: ${e.message}")
|
||||||
|
onError(e.message ?: "Failed to send binary data")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +76,8 @@ class WebSocketManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isConnected(): Boolean = webSocket != null
|
||||||
|
|
||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
Log.d("WebSocket", "Connected!")
|
Log.d("WebSocket", "Connected!")
|
||||||
onConnected()
|
onConnected()
|
||||||
@@ -76,6 +88,7 @@ class WebSocketManager(
|
|||||||
onMessage(text)
|
onMessage(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
webSocket.close(1000, null)
|
webSocket.close(1000, null)
|
||||||
Log.d("WebSocket", "Closing: $code $reason")
|
Log.d("WebSocket", "Closing: $code $reason")
|
||||||
|
|||||||
Reference in New Issue
Block a user