global fix

This commit is contained in:
2025-12-03 20:46:36 +09:00
parent 74bafd4cb1
commit 752b2fb1ca
8 changed files with 95 additions and 68 deletions

File diff suppressed because one or more lines are too long

View File

@@ -19,6 +19,7 @@
android:required="false" />
<application
android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"

View File

@@ -27,8 +27,10 @@ class CameraManager(private val context: Context) {
cameraProviderFuture.addListener(
{
Log.d("CameraManager", "cameraProviderFuture listener invoked")
try {
cameraProvider = cameraProviderFuture.get()
Log.d("CameraManager", "cameraProvider obtained: $cameraProvider")
// Create preview
val preview = Preview.Builder()
@@ -45,6 +47,7 @@ class CameraManager(private val context: Context) {
// Select back camera
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
Log.d("CameraManager", "Using camera selector: $cameraSelector")
// Unbind all use cases
cameraProvider?.unbindAll()
@@ -57,6 +60,7 @@ class CameraManager(private val context: Context) {
imageCapture
)
Log.d("CameraManager", "bindToLifecycle called")
Log.d("CameraManager", "Camera started successfully")
} catch (exc: Exception) {
Log.e("CameraManager", "Use case binding failed", exc)
@@ -105,4 +109,3 @@ class CameraManager(private val context: Context) {
}
}
}

View File

@@ -6,7 +6,6 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.camera.view.PreviewView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -20,7 +19,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Call
import androidx.compose.material.icons.filled.CallEnd
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -31,19 +29,22 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.DisposableEffect
import android.util.Log
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.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.camera.view.PreviewView
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -85,13 +86,8 @@ class MainActivity : ComponentActivity() {
@Composable
fun StreamingApp(modifier: Modifier = Modifier) {
val context = LocalContext.current
PermissionChecker(
onPermissionsGranted = {
// Разрешения выданы, показываем основное приложение
StreamingAppContent(modifier)
}
onPermissionsGranted = { /* permissions granted callback - do not call composables here */ }
) {
// Если разрешения не выданы, PermissionChecker сам покажет запрос
StreamingAppContent(modifier)
@@ -112,8 +108,6 @@ fun StreamingAppContent(modifier: Modifier = Modifier) {
var password by remember { mutableStateOf("") }
var showConnectionForm by remember { mutableStateOf(true) }
val context = LocalContext.current
Surface(
modifier = modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
@@ -276,24 +270,19 @@ fun StreamingScreen(
onDisconnect: () -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraManager = remember { CameraManager(context) }
// Проверяем разрешения на камеру
val hasCameraPermission = PermissionManager.hasCameraPermission(context)
val hasInternetPermission = PermissionManager.hasInternetPermission(context)
LaunchedEffect(isCameraRunning) {
if (isCameraRunning && hasCameraPermission) {
// Start camera preview when connected
// In a real app, would bind to lifecycle and PreviewView
}
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
// Camera preview placeholder
// Camera preview area using PreviewView
Box(
modifier = Modifier
.fillMaxWidth()
@@ -340,13 +329,36 @@ fun StreamingScreen(
)
}
} else if (isCameraRunning) {
// Camera preview would be rendered here
// Using AndroidView with PreviewView in a real implementation
Text(
text = "🎥 Camera Preview",
color = Color.White,
style = MaterialTheme.typography.headlineSmall
// Render PreviewView via AndroidView and start camera
var previewViewRef: PreviewView? by remember { mutableStateOf(null) }
AndroidView(
factory = { ctx ->
PreviewView(ctx).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
// Keep reference for starting camera
previewViewRef = this
}
},
modifier = Modifier
.fillMaxSize()
)
// Start/stop camera based on lifecycle and preview availability
DisposableEffect(previewViewRef, isCameraRunning) {
val pv = previewViewRef
if (pv != null && isCameraRunning) {
cameraManager.startCamera(
lifecycleOwner,
pv.surfaceProvider,
onError = { err -> Log.e("CameraManager", "Camera error: $err") }
)
}
onDispose {
cameraManager.stopCamera()
}
}
} else {
Text(
text = "Camera Inactive",

View File

@@ -9,7 +9,10 @@ data class ServerConnectionConfig(
val password: String
) {
fun getWebSocketUrl(): String {
return "ws://$serverHost:$serverPort/ws/client/$roomId/$password"
// URL-encode roomId and password to handle special characters and avoid accidental mismatches
val encRoom = try { java.net.URLEncoder.encode(roomId, "UTF-8") } catch (e: Exception) { roomId }
val encPass = try { java.net.URLEncoder.encode(password, "UTF-8") } catch (e: Exception) { password }
return "ws://$serverHost:$serverPort/ws/client/$encRoom/$encPass"
}
}
@@ -41,4 +44,3 @@ object VideoCommands {
fun adjustQuality(quality: Int) = VideoCommand(type = "adjust_quality", quality = quality)
fun reset() = VideoCommand(type = "reset")
}

View File

@@ -98,8 +98,34 @@ class StreamViewModel : ViewModel() {
private fun onMessage(message: String) {
Log.d("StreamViewModel", "Message received: $message")
viewModelScope.launch {
if (!message.contains("ping")) {
updateStatus("Получено: $message")
try {
// Try to parse as ConnectionResponse
val gson = com.google.gson.Gson()
val connectionResponse = try {
gson.fromJson(message, ConnectionResponse::class.java)
} catch (e: Exception) {
null
}
if (connectionResponse != null && (connectionResponse.client_id != null || connectionResponse.success)) {
Log.d("StreamViewModel", "ConnectionResponse: success=${connectionResponse.success}, client_id=${connectionResponse.client_id}, room_id=${connectionResponse.room_id}, error=${connectionResponse.error}")
if (connectionResponse.success) {
updateStatus("Сессия создана. Client ID: ${connectionResponse.client_id ?: "-"}")
} else {
updateStatus("Ошибка на сервере: ${connectionResponse.error ?: "unknown"}")
_connectionState.value = ConnectionState.Error(connectionResponse.error ?: "Server error")
}
} else {
// General message handling
if (!message.contains("ping")) {
updateStatus("Получено: $message")
}
}
} catch (e: Exception) {
Log.e("StreamViewModel", "Failed to handle message: ${e.message}")
if (!message.contains("ping")) {
updateStatus("Получено: $message")
}
}
}
}
@@ -173,4 +199,3 @@ sealed class ConnectionState {
object Disconnected : ConnectionState()
data class Error(val message: String) : ConnectionState()
}

View File

@@ -79,7 +79,16 @@ class WebSocketManager(
fun isConnected(): Boolean = webSocket != null
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d("WebSocket", "Connected!")
Log.d("WebSocket", "Connected! Response code=${response.code}, message=${response.message}")
try {
val headers = response.headers
for (i in 0 until headers.size) {
Log.d("WebSocket", "Header: ${headers.name(i)}=${headers.value(i)}")
}
Log.d("WebSocket", "Request url: ${response.request.url}")
} catch (e: Exception) {
Log.w("WebSocket", "Failed to log headers: ${e.message}")
}
onConnected()
}
@@ -105,4 +114,3 @@ class WebSocketManager(
onDisconnected()
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Allow cleartext for local development host and base-config for debug devices -->
<base-config cleartextTrafficPermitted="true" />
<!-- Allow cleartext specifically for the local server IP (if needed) -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">192.168.0.112</domain>
</domain-config>
</network-security-config>