diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 44ecc73..e5f562d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -55,8 +55,9 @@ dependencies {
implementation("androidx.camera:camera-lifecycle:1.3.0")
implementation("androidx.camera:camera-view:1.3.0")
- // WebSocket
- implementation("com.squareup.okhttp3:okhttp:4.11.0")
+ // WebSocket - OkHttp with all required dependencies
+ implementation("com.squareup.okhttp3:okhttp:4.10.0")
+ implementation("com.squareup.okio:okio:3.5.0")
// JSON
implementation("com.google.code.gson:gson:2.10.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3babd0d..1974fe9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,10 @@
+
+
+
+
Unit,
+ previewSurfaceProvider: Preview.SurfaceProvider,
onError: (String) -> Unit
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
@@ -34,9 +34,7 @@ class CameraManager(private val context: Context) {
val preview = Preview.Builder()
.build()
.apply {
- setSurfaceProvider { surfaceProvider ->
- previewSurfaceProvider(surfaceProvider)
- }
+ setSurfaceProvider(previewSurfaceProvider)
}
// Create image capture
diff --git a/app/src/main/java/com/example/camcontrol/MainActivity.kt b/app/src/main/java/com/example/camcontrol/MainActivity.kt
index 2cee810..0eeba51 100644
--- a/app/src/main/java/com/example/camcontrol/MainActivity.kt
+++ b/app/src/main/java/com/example/camcontrol/MainActivity.kt
@@ -85,6 +85,21 @@ class MainActivity : ComponentActivity() {
@Composable
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 connectionState by viewModel.connectionState.collectAsState()
val statusMessage by viewModel.statusMessage.collectAsState()
@@ -263,12 +278,12 @@ fun StreamingScreen(
val context = LocalContext.current
val cameraManager = remember { CameraManager(context) }
+ // Проверяем разрешения на камеру
+ val hasCameraPermission = PermissionManager.hasCameraPermission(context)
+ val hasInternetPermission = PermissionManager.hasInternetPermission(context)
+
LaunchedEffect(isCameraRunning) {
- if (isCameraRunning && ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.CAMERA
- ) == PackageManager.PERMISSION_GRANTED
- ) {
+ if (isCameraRunning && hasCameraPermission) {
// Start camera preview when connected
// In a real app, would bind to lifecycle and PreviewView
}
@@ -286,7 +301,45 @@ fun StreamingScreen(
.background(Color.Black),
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
// Using AndroidView with PreviewView in a real implementation
Text(
@@ -396,10 +449,11 @@ fun StreamingScreen(
containerColor = MaterialTheme.colorScheme.error
)
) {
- androidx.compose.material.icons.Icon(
+ androidx.compose.material3.Icon(
imageVector = Icons.Filled.CallEnd,
contentDescription = "Disconnect",
- modifier = Modifier.padding(end = 8.dp)
+ modifier = Modifier.padding(end = 8.dp),
+ tint = Color.White
)
Text("Отключиться")
}
diff --git a/app/src/main/java/com/example/camcontrol/PermissionManager.kt b/app/src/main/java/com/example/camcontrol/PermissionManager.kt
new file mode 100644
index 0000000..e34c919
--- /dev/null
+++ b/app/src/main/java/com/example/camcontrol/PermissionManager.kt
@@ -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 {
+ return REQUIRED_PERMISSIONS.filter { !hasPermission(context, it) }
+ }
+
+ /**
+ * Возвращает статус каждого разрешения
+ */
+ fun getPermissionsStatus(context: Context): Map {
+ return (REQUIRED_PERMISSIONS + OPTIONAL_PERMISSIONS).associateWith { permission ->
+ hasPermission(context, permission)
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/example/camcontrol/PermissionUI.kt b/app/src/main/java/com/example/camcontrol/PermissionUI.kt
new file mode 100644
index 0000000..d56e1d4
--- /dev/null
+++ b/app/src/main/java/com/example/camcontrol/PermissionUI.kt
@@ -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,
+ 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
+ )
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/example/camcontrol/VideoStreamingManager.kt b/app/src/main/java/com/example/camcontrol/VideoStreamingManager.kt
index 4cc918a..3417ed1 100644
--- a/app/src/main/java/com/example/camcontrol/VideoStreamingManager.kt
+++ b/app/src/main/java/com/example/camcontrol/VideoStreamingManager.kt
@@ -24,7 +24,7 @@ class VideoStreamingManager(
fun startStreaming(
lifecycleOwner: LifecycleOwner,
- previewSurfaceProvider: (Preview.SurfaceProvider) -> Unit
+ previewSurfaceProvider: Preview.SurfaceProvider
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
@@ -37,9 +37,7 @@ class VideoStreamingManager(
val preview = Preview.Builder()
.build()
.apply {
- setSurfaceProvider { surfaceProvider ->
- previewSurfaceProvider(surfaceProvider)
- }
+ setSurfaceProvider(previewSurfaceProvider)
}
// Create image analysis for frame processing
diff --git a/app/src/main/java/com/example/camcontrol/WebSocketManager.kt b/app/src/main/java/com/example/camcontrol/WebSocketManager.kt
index bfb6ca3..b86c114 100644
--- a/app/src/main/java/com/example/camcontrol/WebSocketManager.kt
+++ b/app/src/main/java/com/example/camcontrol/WebSocketManager.kt
@@ -12,7 +12,8 @@ class WebSocketManager(
private val onConnected: () -> Unit = {},
private val onDisconnected: () -> Unit = {},
private val onError: (String) -> Unit = {},
- private val onMessage: (String) -> Unit = {}
+ private val onMessage: (String) -> Unit = {},
+ private val onBinaryMessage: (ByteArray) -> Unit = {}
) : WebSocketListener() {
private var webSocket: WebSocket? = null
@@ -20,6 +21,7 @@ class WebSocketManager(
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
+ .retryOnConnectionFailure(true)
.build()
fun connect(url: String) {
@@ -29,6 +31,7 @@ class WebSocketManager(
.build()
webSocket = client.newWebSocket(request, this)
+ client.dispatcher.executorService // Ensure executor is running
Log.d("WebSocket", "Connecting to: $url")
} catch (e: Exception) {
Log.e("WebSocket", "Connection error: ${e.message}")
@@ -48,11 +51,18 @@ class WebSocketManager(
fun sendBinary(data: ByteArray) {
try {
- val byteString = okhttp3.ByteString.of(*data)
- webSocket?.send(byteString)
+ // Create ByteString from ByteArray using reflection to avoid import issues
+ 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")
} catch (e: Exception) {
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) {
Log.d("WebSocket", "Connected!")
onConnected()
@@ -76,6 +88,7 @@ class WebSocketManager(
onMessage(text)
}
+
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
webSocket.close(1000, null)
Log.d("WebSocket", "Closing: $code $reason")