launch fix
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("Отключиться")
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user