launch fix

This commit is contained in:
2025-12-03 20:06:46 +09:00
parent 2bc018a4f7
commit 74bafd4cb1
8 changed files with 425 additions and 21 deletions

View File

@@ -5,6 +5,10 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<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 -->
<uses-feature

View File

@@ -20,7 +20,7 @@ class CameraManager(private val context: Context) {
fun startCamera(
lifecycleOwner: LifecycleOwner,
previewSurfaceProvider: (Preview.SurfaceProvider) -> 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

View File

@@ -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("Отключиться")
}

View File

@@ -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)
}
}
}

View 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
)
}
}
}

View File

@@ -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

View File

@@ -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")