main commit

This commit is contained in:
2025-10-06 09:40:51 +09:00
parent b1de55d253
commit 79256cd9fc
2375 changed files with 370050 additions and 4033 deletions

View File

@@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-10-04T02:36:57.095708689Z">
<DropdownSelection timestamp="2025-10-05T21:09:42.524082016Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=LGMG600S9b4da66b" />

7
.idea/misc.xml generated
View File

@@ -6,4 +6,11 @@
<component name="ProjectType">
<option name="id" value="Android" />
</component>
<component name="VisualizationToolProject">
<option name="state">
<ProjectState>
<option name="scale" value="0.19048339843749995" />
</ProjectState>
</option>
</component>
</project>

View File

@@ -0,0 +1,78 @@
kotlin version: 2.0.20
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/GodEyeApplication.kt:15:5: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
... 35 more

View File

@@ -0,0 +1,78 @@
kotlin version: 2.0.20
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/GodEyeApplication.kt:15:5: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
... 35 more

View File

@@ -17,6 +17,12 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
sourceSets {
getByName("main") {
manifest.srcFile("src/main/AndroidManifest-legacy.xml")
}
}
buildTypes {
release {
isMinifyEnabled = false

View File

@@ -1,18 +1,19 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.godeye"
compileSdk = 29 // Понижаем до Android 10
compileSdk = 36
defaultConfig {
applicationId = "com.example.godeye"
minSdk = 24
targetSdk = 28 // Понижаем до Android 9
targetSdk = 36
versionCode = 1
versionName = "1.0-legacy"
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -28,60 +29,72 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "11"
}
// ОТКЛЮЧАЕМ COMPOSE ДЛЯ LEGACY ВЕРСИИ
buildFeatures {
compose = false
compose = true
viewBinding = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.15"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
// ЭКСТРЕМАЛЬНО СТАРЫЕ зависимости для Android 9 (compileSdk 29)
implementation("androidx.core:core-ktx:1.3.2") // Совместимо с API 29
implementation("androidx.appcompat:appcompat:1.2.0") // Совместимо с API 29
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.2.0") // Совместимо с API 29
// Core Android
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.preference)
// Классический Android UI - версии для API 29
implementation("com.google.android.material:material:1.3.0") // Совместимо с API 29
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
implementation("androidx.fragment:fragment-ktx:1.2.5") // Совместимо с API 29
implementation("androidx.cardview:cardview:1.0.0") // Совместимо с API 29
implementation("androidx.activity:activity-ktx:1.1.0") // Совместимо с API 29
// Compose
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
// СТАРЫЕ ViewModel версии для API 29
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0") // Совместимо с API 29
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0") // Совместимо с API 29
// Material Icons
implementation("androidx.compose.material:material-icons-core")
implementation("androidx.compose.material:material-icons-extended")
// УБИРАЕМ СОВРЕМЕННЫЕ CAMERA БИБЛИОТЕКИ
// Вместо CameraX используем старую Camera2 API напрямую
// Coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
// Socket.IO и базовые сетевые библиотеки
implementation("io.socket:socket.io-client:2.1.0")
implementation("com.google.code.gson:gson:2.8.9") // Старая версия
// Network
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
implementation(libs.socketio.client)
// УБИРАЕМ WebRTC полностью для стабильности
// implementation("io.getstream:stream-webrtc-android:1.0.4")
// JSON
implementation(libs.gson)
// Старые корутины
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") // 2021 год
// Базовые зависимости - старые версии
implementation("androidx.recyclerview:recyclerview:1.2.1") // 2021 год
// УБИРАЕМ Work Manager и Activity KTX
// implementation("androidx.work:work-runtime-ktx:2.8.1")
// implementation("androidx.activity:activity-ktx:1.7.2")
// WebRTC
implementation(libs.webrtc.android)
// Testing
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3") // Старая версия
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") // Старая версия
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

View File

@@ -0,0 +1,87 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.godeye"
compileSdk = 29 // Понижаем до Android 10
defaultConfig {
applicationId = "com.example.godeye"
minSdk = 24
targetSdk = 28 // Понижаем до Android 9
versionCode = 1
versionName = "1.0-legacy"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
// ОТКЛЮЧАЕМ COMPOSE ДЛЯ LEGACY ВЕРСИИ
buildFeatures {
compose = false
viewBinding = true
}
}
dependencies {
// ЭКСТРЕМАЛЬНО СТАРЫЕ зависимости для Android 9 (compileSdk 29)
implementation("androidx.core:core-ktx:1.3.2") // Совместимо с API 29
implementation("androidx.appcompat:appcompat:1.2.0") // Совместимо с API 29
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.2.0") // Совместимо с API 29
// Классический Android UI - версии для API 29
implementation("com.google.android.material:material:1.3.0") // Совместимо с API 29
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
implementation("androidx.fragment:fragment-ktx:1.2.5") // Совместимо с API 29
implementation("androidx.cardview:cardview:1.0.0") // Совместимо с API 29
implementation("androidx.activity:activity-ktx:1.1.0") // Совместимо с API 29
// СТАРЫЕ ViewModel версии для API 29
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0") // Совместимо с API 29
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0") // Совместимо с API 29
// УБИРАЕМ СОВРЕМЕННЫЕ CAMERA БИБЛИОТЕКИ
// Вместо CameraX используем старую Camera2 API напрямую
// Socket.IO и базовые сетевые библиотеки
implementation("io.socket:socket.io-client:2.1.0")
implementation("com.google.code.gson:gson:2.8.9") // Старая версия
// УБИРАЕМ WebRTC полностью для стабильности
// implementation("io.getstream:stream-webrtc-android:1.0.4")
// Старые корутины
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") // 2021 год
// Базовые зависимости - старые версии
implementation("androidx.recyclerview:recyclerview:1.2.1") // 2021 год
// УБИРАЕМ Work Manager и Activity KTX
// implementation("androidx.work:work-runtime-ktx:2.8.1")
// implementation("androidx.activity:activity-ktx:1.7.2")
// Testing
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3") // Старая версия
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") // Старая версия
}

View File

@@ -2,15 +2,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Разрешения согласно ТЗ -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Разрешения для сигналлинга и WebRTC -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Hardware features согласно ТЗ -->
<!-- Разрешения для камеры и микрофона -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- Hardware features -->
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />
@@ -26,9 +33,9 @@
android:theme="@style/Theme.GodEye"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31">
tools:targetApi="35">
<!-- MainActivity - главный экран согласно ТЗ -->
<!-- MainActivity с Compose интерфейсом -->
<activity
android:name=".MainActivity"
android:exported="true"
@@ -41,7 +48,7 @@
</intent-filter>
</activity>
<!-- LegacyMainActivity - упрощенная версия для Android 9 -->
<!-- Legacy активности для совместимости -->
<activity
android:name=".LegacyMainActivity"
android:exported="false"
@@ -49,7 +56,6 @@
android:screenOrientation="portrait"
android:launchMode="singleTop" />
<!-- LegacyCameraActivity - камера для Android 9 -->
<activity
android:name=".LegacyCameraActivity"
android:exported="false"
@@ -57,12 +63,24 @@
android:screenOrientation="portrait"
android:launchMode="singleTop" />
<!-- SocketService - WebSocket соединение согласно ТЗ -->
<!-- Сервисы -->
<service
android:name=".services.SocketService"
android:enabled="true"
android:exported="false" />
<service
android:name=".services.SignalingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".services.ConnectionService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
</manifest>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Разрешения согласно ТЗ -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Hardware features согласно ТЗ -->
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />
<application
android:name=".GodEyeApplication"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GodEye"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<!-- MainActivity - главный экран согласно ТЗ -->
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.GodEye"
android:screenOrientation="portrait"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- LegacyMainActivity - упрощенная версия для Android 9 -->
<activity
android:name=".LegacyMainActivity"
android:exported="false"
android:theme="@style/Theme.GodEye"
android:screenOrientation="portrait"
android:launchMode="singleTop" />
<!-- LegacyCameraActivity - камера для Android 9 -->
<activity
android:name=".LegacyCameraActivity"
android:exported="false"
android:theme="@style/Theme.GodEye"
android:screenOrientation="portrait"
android:launchMode="singleTop" />
<!-- SocketService - WebSocket соединение согласно ТЗ -->
<service
android:name=".services.SocketService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

View File

@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.widget.Toast
@@ -128,7 +129,11 @@ class LegacyMainActivity : AppCompatActivity() {
private fun startAndBindService() {
try {
val intent = Intent(this, SocketService::class.java)
startForegroundService(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
Logger.step("LEGACY_SERVICE_BIND", "Binding to SocketService")
} catch (e: Exception) {

View File

@@ -1,431 +1,176 @@
package com.example.godeye
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.godeye.camera.CameraScreen
import com.example.godeye.managers.PermissionManager
import androidx.lifecycle.lifecycleScope
import com.example.godeye.managers.ConnectionManager
import com.example.godeye.managers.AutoApprovalManager
import com.example.godeye.models.*
import com.example.godeye.services.SocketService
import com.example.godeye.services.ConnectionService
import com.example.godeye.ui.components.*
import com.example.godeye.ui.theme.GodEyeColors
import com.example.godeye.ui.screens.MainScreen
import com.example.godeye.ui.screens.SettingsScreen
import com.example.godeye.ui.theme.GodEyeTheme
import com.example.godeye.utils.ErrorHandler
import com.example.godeye.utils.Logger
import com.example.godeye.utils.PermissionHelper
import com.example.godeye.utils.PreferenceManager
import kotlinx.coroutines.launch
/**
* MainActivity - упрощенная версия для Android 9
* БЕЗ сложных анимаций и градиентов
*/
@OptIn(ExperimentalMaterial3Api::class)
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
private val errorHandler = ErrorHandler()
private var socketService: SocketService? = null
// Подключение к SocketService
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Logger.step("SERVICE_CONNECTED", "SocketService connected to MainActivity")
val binder = service as SocketService.LocalBinder
socketService = binder.getService()
viewModel.bindToSocketService(socketService!!)
}
override fun onServiceDisconnected(name: ComponentName?) {
Logger.step("SERVICE_DISCONNECTED", "SocketService disconnected from MainActivity")
socketService = null
}
}
// Обработка разрешений
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
Logger.step("PERMISSIONS_RESULT", "Permission request result received")
val allGranted = permissions.values.all { it }
if (allGranted) {
Logger.step("PERMISSIONS_ALL_GRANTED", "All permissions granted")
viewModel.onPermissionsGranted()
} else {
val denied = permissions.filterValues { !it }.keys
Logger.step("PERMISSIONS_DENIED", "Some permissions denied: ${denied.joinToString()}")
val permissionManager = PermissionManager(this)
val hasCritical = denied.any { it in PermissionManager.CRITICAL_PERMISSIONS }
if (hasCritical) {
errorHandler.handleError(AppError.CameraPermissionDenied, this)
}
}
}
// Убираем SignalingManager - используем только ConnectionManager + AutoApprovalManager
// private lateinit var signalingManager: SignalingManager
private lateinit var connectionManager: ConnectionManager
private lateinit var autoApprovalManager: AutoApprovalManager
private lateinit var permissionHelper: PermissionHelper
private lateinit var preferenceManager: PreferenceManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
Logger.step("ACTIVITY_CREATE", "MainActivity onCreate simplified for Android 9")
Logger.d("Device: ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}")
Logger.d("Android: ${android.os.Build.VERSION.RELEASE}")
// Инициализируем менеджеры (БЕЗ SignalingManager)
preferenceManager = PreferenceManager(this)
// signalingManager = SignalingManager(this) // УБИРАЕМ
connectionManager = ConnectionManager(this, preferenceManager)
autoApprovalManager = AutoApprovalManager(this, preferenceManager, connectionManager)
permissionHelper = PermissionHelper(this)
// Запуск SocketService
startAndBindSocketService()
setContent {
GodEyeTheme {
var showSettings by remember { mutableStateOf(false) }
var showCamera by remember { mutableStateOf(false) }
val cameraRequest by viewModel.cameraRequest.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
// Автоматическое принятие запросов камеры
LaunchedEffect(cameraRequest) {
val currentRequest = cameraRequest
if (currentRequest != null) {
Logger.step("AUTO_ACCEPT_CAMERA_REQUEST", "Auto-accepting camera request")
showCamera = true
viewModel.acceptCameraRequest(currentRequest.sessionId, "Auto-accepted")
} else {
showCamera = false
}
}
// Обработка ошибок
val connectionState by viewModel.connectionState.collectAsState()
LaunchedEffect(connectionState) {
if (connectionState == ConnectionState.ERROR) {
errorHandler.handleError(AppError.NetworkError, this@MainActivity, scope, snackbarHostState)
}
}
Box(modifier = Modifier.fillMaxSize()) {
when {
showCamera && cameraRequest != null -> {
Logger.step("UI_RENDERING_CAMERA", "Rendering simplified CameraScreen")
CameraScreen(
onBackPressed = {
Logger.step("CAMERA_BACK_PRESSED", "User pressed back")
showCamera = false
viewModel.clearCameraRequest()
},
sessionId = cameraRequest!!.sessionId,
operatorId = cameraRequest!!.operatorId
)
}
showSettings -> {
Logger.step("UI_RENDERING_SETTINGS", "Rendering SettingsScreen")
SettingsScreen(
onBackPressed = { showSettings = false },
onServerConfigSaved = { url ->
viewModel.updateServerUrl(url)
showSettings = false
viewModel.connectToServer()
}
)
}
else -> {
Logger.step("UI_RENDERING_MAIN", "Rendering simplified MainScreen")
SimplifiedMainScreen(
onSettingsClick = { showSettings = true },
onCameraAccept = { showCamera = true },
snackbarHostState = snackbarHostState
)
}
}
// Простой Snackbar для ошибок
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
setContent {
GodEyeTheme {
AppContent()
}
// Проверка разрешений
checkRequiredPermissions()
Logger.step("ACTIVITY_CREATE_COMPLETE", "MainActivity simplified onCreate complete")
} catch (e: Exception) {
Logger.error("ACTIVITY_CREATE_ERROR", "Error in MainActivity onCreate", e)
errorHandler.handleError(AppError.UnknownError(e), this)
}
}
private fun startAndBindSocketService() {
Logger.step("SOCKET_SERVICE_START", "Starting SocketService")
val intent = Intent(this, SocketService::class.java)
startForegroundService(intent)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
// Проверяем разрешения при запуске
checkPermissionsAndStart()
private fun checkRequiredPermissions() {
Logger.step("PERMISSION_CHECK", "Checking permissions")
val permissionManager = PermissionManager(this)
if (!permissionManager.checkPermissions()) {
val missingPermissions = permissionManager.getMissingPermissions()
Logger.step("PERMISSIONS_MISSING", "Requesting: ${missingPermissions.joinToString()}")
permissionLauncher.launch(missingPermissions)
} else {
Logger.step("PERMISSIONS_OK", "All permissions granted")
viewModel.onPermissionsGranted()
}
// Запускаем фоновый сервис
startConnectionService()
}
@Composable
private fun SimplifiedMainScreen(
onSettingsClick: () -> Unit,
onCameraAccept: () -> Unit,
snackbarHostState: SnackbarHostState
) {
val connectionState by viewModel.connectionState.collectAsState()
val serverUrl by viewModel.serverUrl.collectAsState()
val deviceId by viewModel.deviceId.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
val cameraRequest by viewModel.cameraRequest.collectAsState()
val isStreaming by viewModel.isStreaming.collectAsState()
val activeSessions by viewModel.activeSessions.collectAsState()
val permissionsGranted by viewModel.permissionsGranted.collectAsState()
val scope = rememberCoroutineScope()
private fun AppContent() {
var currentScreen by remember { mutableStateOf(Screen.Main) }
// УПРОЩЕННЫЙ UI ДЛЯ ANDROID 9 - БЕЗ СЛОЖНЫХ АНИМАЦИЙ
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Простой заголовок
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.Gray.copy(alpha = 0.3f))
) {
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "GodEye Signal Center",
style = MaterialTheme.typography.headlineMedium,
color = Color.White
)
Text(
text = "Android Client v1.0 (Simplified)",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
}
}
// Разрешения
if (!permissionsGranted) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.Red.copy(alpha = 0.7f))
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("⚠️ Требуются разрешения", color = Color.White)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
val permissionManager = PermissionManager(this@MainActivity)
permissionLauncher.launch(permissionManager.getMissingPermissions())
}
) {
Text("Предоставить разрешения")
}
}
}
}
// Device ID
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.Blue.copy(alpha = 0.3f))
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("📱 Device ID", color = Color.White)
Text(deviceId.take(16) + "...", color = Color.Gray)
Text("${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}", color = Color.Gray)
}
}
// Подключение к серверу
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = when (connectionState) {
ConnectionState.CONNECTED -> Color.Green.copy(alpha = 0.3f)
ConnectionState.ERROR -> Color.Red.copy(alpha = 0.3f)
else -> Color.Yellow.copy(alpha = 0.3f)
}
when (currentScreen) {
Screen.Main -> {
MainScreen(
connectionManager = connectionManager,
autoApprovalManager = autoApprovalManager,
preferenceManager = preferenceManager,
permissionHelper = permissionHelper,
onOpenSettings = { currentScreen = Screen.Settings }
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("🌐 Сервер", color = Color.White)
Text(
when (connectionState) {
ConnectionState.CONNECTED -> "✅ Подключено"
ConnectionState.CONNECTING -> "🔄 Подключение..."
ConnectionState.ERROR -> "❌ Ошибка"
else -> "⚪ Отключено"
},
color = Color.White
)
Text("$serverUrl", color = Color.Gray)
Spacer(modifier = Modifier.height(8.dp))
if (connectionState == ConnectionState.DISCONNECTED) {
Button(
onClick = {
if (permissionsGranted) {
viewModel.connectToServer()
} else {
scope.launch {
snackbarHostState.showSnackbar("Нужны разрешения")
}
}
},
enabled = !isLoading,
modifier = Modifier.fillMaxWidth()
) {
Text(if (isLoading) "Подключение..." else "🔗 Подключиться")
}
} else {
Button(
onClick = viewModel::disconnect,
modifier = Modifier.fillMaxWidth()
) {
Text("❌ Отключиться")
}
}
}
}
// Статус трансляции
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (isStreaming) Color.Green.copy(alpha = 0.3f) else Color.Gray.copy(alpha = 0.3f)
Screen.Settings -> {
SettingsScreen(
preferenceManager = preferenceManager,
permissionHelper = permissionHelper,
onNavigateBack = { currentScreen = Screen.Main }
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("📹 Трансляция", color = Color.White)
Text(
if (isStreaming) "🔴 Активна: ${activeSessions.size} сессий" else "⚪ Неактивна",
color = Color.White
)
}
}
}
}
// Кнопки управления
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = onCameraAccept,
enabled = permissionsGranted,
modifier = Modifier.weight(1f)
) {
Text("📷 Камера")
}
Button(
onClick = onSettingsClick,
modifier = Modifier.weight(1f)
) {
Text("⚙️ Настройки")
}
}
private enum class Screen {
Main, Settings
}
// Кнопка для запуска Legacy версии
Button(
onClick = {
Logger.step("LAUNCH_LEGACY_VERSION", "Launching LegacyMainActivity")
val intent = Intent(this@MainActivity, LegacyMainActivity::class.java)
startActivity(intent)
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF9C27B0) // Фиолетовый цвет для выделения
)
) {
Text("📱 Legacy версия (Android 9)")
}
// Запрос от оператора
cameraRequest?.let { request ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.Yellow.copy(alpha = 0.8f))
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("📞 Запрос от оператора", color = Color.Black)
Text("Сессия: ${request.sessionId.take(8)}...", color = Color.Black)
Spacer(modifier = Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = {
viewModel.acceptCameraRequest(request.sessionId, "Принято")
onCameraAccept()
},
modifier = Modifier.weight(1f)
) {
Text("✅ Принять")
}
Button(
onClick = {
viewModel.rejectCameraRequest(request.sessionId, "Отклонено")
},
modifier = Modifier.weight(1f)
) {
Text("❌ Отклонить")
}
}
}
private fun checkPermissionsAndStart() {
if (permissionHelper.hasAllPermissions()) {
// Все разрешения уже есть, можно работать
Log.d("MainActivity", "All permissions granted")
startApplication()
} else {
// Запрашиваем разрешения
Log.d("MainActivity", "Requesting permissions")
permissionHelper.requestPermissions { granted ->
if (granted) {
Log.d("MainActivity", "Permissions granted, starting application")
startApplication()
} else {
Log.w("MainActivity", "Some permissions denied")
}
}
}
}
private fun startApplication() {
// Инициализируем автоматическое подключение если включено
if (preferenceManager.getAutoConnect()) {
connectionManager.connect()
}
Log.d("MainActivity", "Application started with settings: ${preferenceManager.getAllSettings()}")
}
/**
* Запуск фонового сервиса для поддержания подключения
*/
private fun startConnectionService() {
val serviceIntent = Intent(this, ConnectionService::class.java).apply {
action = ConnectionService.ACTION_START
}
// Проверяем версию Android для совместимости
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
Log.d("MainActivity", "Connection service started")
}
/**
* Остановка фонового сервиса
*/
private fun stopConnectionService() {
val serviceIntent = Intent(this, ConnectionService::class.java).apply {
action = ConnectionService.ACTION_STOP
}
stopService(serviceIntent)
Log.d("MainActivity", "Connection service stopped")
}
override fun onResume() {
super.onResume()
Log.d("MainActivity", "Activity resumed")
// Проверяем настройки и автоподключение
if (permissionHelper.hasAllPermissions() && preferenceManager.getAutoConnect()) {
connectionManager.connect()
}
}
override fun onPause() {
super.onPause()
Log.d("MainActivity", "Activity paused - service continues in background")
}
override fun onDestroy() {
Logger.step("ACTIVITY_DESTROY", "MainActivity destroyed")
try {
unbindService(serviceConnection)
} catch (e: Exception) {
Logger.error("UNBIND_SERVICE_ERROR", "Error unbinding SocketService", e)
}
super.onDestroy()
// Очищаем ресурсы Activity, но оставляем сервис работать
connectionManager.cleanup()
autoApprovalManager.cleanup()
// Останавливаем сервис только если приложение действительно закрывается
if (isFinishing) {
stopConnectionService()
}
Log.d("MainActivity", "MainActivity destroyed, resources cleaned up")
}
}

View File

@@ -15,7 +15,6 @@ import com.example.godeye.services.SocketService
import com.example.godeye.utils.Logger
import com.example.godeye.utils.generateDeviceId
import com.example.godeye.utils.getPreferences
import com.example.godeye.webrtc.WebRTCManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -131,53 +130,62 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
/**
* Настройка наблюдателей за SocketService
* Настройка наблюдателей за SocketService - исправленная версия
*/
private fun setupServiceObservers() {
val service = socketService ?: return
// Простое наблюдение за состоянием подключения без внутренних API
viewModelScope.launch {
// Наблюдение за состоянием подключения
service.connectionState.collect { state ->
_connectionState.value = state
Logger.step("CONNECTION_STATE_CHANGED", "Connection state: $state")
}
}
viewModelScope.launch {
// Наблюдение за запросами камеры от операторов
service.cameraRequests.collect { request ->
if (request != null) {
Logger.step("CAMERA_REQUEST_RECEIVED",
"Camera request from ${request.operatorId} for ${request.cameraType}")
_cameraRequest.value = request
while (true) {
try {
val state = service.connectionState.value
if (_connectionState.value != state) {
_connectionState.value = state
Logger.step("CONNECTION_STATE_CHANGED", "Connection state: $state")
}
kotlinx.coroutines.delay(1000) // Проверяем каждую секунду
} catch (e: Exception) {
Logger.error("CONNECTION_STATE_ERROR", "Error checking connection state", e)
break
}
}
}
// Простое наблюдение за запросами камеры
viewModelScope.launch {
// Наблюдение за WebRTC событиями
service.webRTCEvents.collect { event ->
event?.let { handleWebRTCEvent(it) }
while (true) {
try {
val request = service.cameraRequests.value
if (_cameraRequest.value != request && request != null) {
Logger.step("CAMERA_REQUEST_RECEIVED",
"Camera request from ${request.operatorId} for ${request.cameraType}")
_cameraRequest.value = request
}
kotlinx.coroutines.delay(500) // Проверяем каждые полсекунды
} catch (e: Exception) {
Logger.error("CAMERA_REQUEST_ERROR", "Error checking camera requests", e)
break
}
}
}
// Простое наблюдение за WebRTC событиями
viewModelScope.launch {
// Наблюдение за сессиями
sessionManager.sessions.collect { sessions ->
val sessionInfo = sessions.mapValues { (sessionId, session) ->
SessionInfo(
sessionId = sessionId,
deviceId = _deviceId.value,
operatorId = session.operatorId,
cameraType = session.cameraType,
status = if (session.webRTCConnected) "Connected" else "Connecting",
createdAt = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
.format(java.util.Date(session.startTime))
)
while (true) {
try {
val event = service.webRTCEvents.value
if (event != null) {
handleWebRTCEvent(event)
// Очищаем событие после обработки
service.webRTCEvents as MutableStateFlow
(service.webRTCEvents as MutableStateFlow).value = null
}
kotlinx.coroutines.delay(100) // Проверяем каждые 100мс
} catch (e: Exception) {
Logger.error("WEBRTC_EVENT_ERROR", "Error checking WebRTC events", e)
break
}
_activeSessions.value = sessionInfo
_isStreaming.value = sessions.values.any { it.isActive }
}
}
}
@@ -186,10 +194,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* Инициализация WebRTC согласно ТЗ
*/
private fun initializeWebRTC() {
webRTCManager = WebRTCManager(context) { message ->
// Обработка сигнальных сообщений через SocketService
Logger.step("WEBRTC_SIGNALING", "WebRTC signaling message: ${message.getString("type")}")
}
webRTCManager = WebRTCManager(context)
}
/**
@@ -257,7 +262,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
socketService?.sendCameraResponse(sessionId, true, reason)
// 3. Инициализация WebRTC соединения
webRTCManager?.startStreaming(sessionId, request.cameraType)
webRTCManager?.startStreaming(request.operatorId, request.cameraType)
// 4. Очистка запроса
_cameraRequest.value = null
@@ -287,21 +292,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
*/
private fun handleWebRTCEvent(event: com.example.godeye.services.WebRTCEvent) {
when (event) {
is com.example.godeye.services.WebRTCEvent.Offer -> {
Logger.step("WEBRTC_OFFER", "Processing WebRTC offer for session: ${event.sessionId}")
webRTCManager?.handleOffer(event.sessionId, event.offer)
is com.example.godeye.services.WebRTCEvent.Connected -> {
Logger.step("WEBRTC_CONNECTED", "WebRTC connected")
}
is com.example.godeye.services.WebRTCEvent.Answer -> {
Logger.step("WEBRTC_ANSWER", "Processing WebRTC answer for session: ${event.sessionId}")
webRTCManager?.handleAnswer(event.sessionId, event.answer)
is com.example.godeye.services.WebRTCEvent.Disconnected -> {
Logger.step("WEBRTC_DISCONNECTED", "WebRTC disconnected")
}
is com.example.godeye.services.WebRTCEvent.IceCandidate -> {
Logger.step("WEBRTC_ICE", "Processing ICE candidate for session: ${event.sessionId}")
webRTCManager?.handleIceCandidate(event.sessionId, event.candidate, event.sdpMid, event.sdpMLineIndex)
is com.example.godeye.services.WebRTCEvent.Error -> {
Logger.step("WEBRTC_ERROR", "WebRTC error: ${event.message}")
}
is com.example.godeye.services.WebRTCEvent.SwitchCamera -> {
Logger.step("WEBRTC_SWITCH_CAMERA", "Switching camera to: ${event.cameraType}")
switchCamera(event.cameraType)
else -> {
Logger.step("WEBRTC_EVENT", "Unknown WebRTC event: $event")
}
}
}
@@ -335,7 +336,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
try {
Logger.step("SWITCH_CAMERA", "Switching camera to: $cameraType")
webRTCManager?.switchCamera(cameraType)
webRTCManager?.switchCamera()
// Обновляем тип камеры в активных сессиях
val updatedSessions = _activeSessions.value.mapValues { (_, sessionInfo) ->
@@ -358,7 +359,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
Logger.step("END_SESSION", "Ending camera session: $sessionId")
sessionManager.endSession(sessionId, "Ended by user")
webRTCManager?.endSession(sessionId)
webRTCManager?.endSession()
Logger.step("SESSION_ENDED", "Session ended: $sessionId")
}
@@ -404,34 +405,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
permissionManager.checkPermissions()
}
/**
* Связывание с SocketService
*/
fun bindToSocketService(service: SocketService) {
Logger.step("VIEWMODEL_BIND_SERVICE", "Binding ViewModel to SocketService")
viewModelScope.launch {
// Наблюдение за состоянием подключения
service.connectionState.collect { state ->
_connectionState.value = state
}
}
viewModelScope.launch {
// Наблюдение за запросами камеры
service.cameraRequests.collect { request ->
_cameraRequest.value = request
}
}
viewModelScope.launch {
// Наблюдение за WebRTC событиями
service.webRTCEvents.collect { event ->
event?.let { handleWebRTCEvent(it) }
}
}
}
/**
* Callback при получении разрешений
*/
@@ -471,14 +444,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Инициализируем WebRTC если нужно
if (webRTCManager == null) {
webRTCManager = WebRTCManager(context) { message ->
// Обработка сигналинга для тестового режима
Logger.d("Test signaling message: $message")
}
webRTCManager = WebRTCManager(context)
}
// Запускаем стриминг
webRTCManager?.startStreaming(testSessionId, "back")
webRTCManager?.startStreaming("test_operator", "back")
Logger.step("START_TEST_STREAMING_SUCCESS", "Test streaming started successfully")

View File

@@ -1,364 +0,0 @@
package com.example.godeye
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.example.godeye.ui.theme.GodEyeColors
import com.example.godeye.utils.getPreferences
/**
* Экран настроек GodEye с расширенными параметрами согласно ТЗ
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onBackPressed: () -> Unit,
onServerConfigSaved: (String) -> Unit
) {
val context = LocalContext.current
val prefs = context.getPreferences()
// Состояния настроек
var serverUrl by remember {
mutableStateOf(prefs.getString("server_url", "http://192.168.219.108:3001") ?: "")
}
var deviceName by remember {
mutableStateOf(prefs.getString("device_name", "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}") ?: "")
}
var autoConnect by remember {
mutableStateOf(prefs.getBoolean("auto_connect", false))
}
var autoAcceptRequests by remember {
mutableStateOf(prefs.getBoolean("auto_accept_requests", true))
}
var enableNotifications by remember {
mutableStateOf(prefs.getBoolean("enable_notifications", true))
}
var keepScreenOn by remember {
mutableStateOf(prefs.getBoolean("keep_screen_on", false))
}
var preferredCamera by remember {
mutableStateOf(prefs.getString("preferred_camera", "back") ?: "back")
}
var streamQuality by remember {
mutableStateOf(prefs.getString("stream_quality", "720p") ?: "720p")
}
Column(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
) {
// Шапка экрана
TopAppBar(
title = {
Text(
text = "Настройки GodEye",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
},
navigationIcon = {
IconButton(onClick = onBackPressed) {
Icon(
Icons.Default.ArrowBack,
contentDescription = "Назад",
tint = GodEyeColors.IvoryPure
)
}
},
actions = {
TextButton(
onClick = {
// Сохраняем все настройки
prefs.edit {
putString("server_url", serverUrl)
putString("device_name", deviceName)
putBoolean("auto_connect", autoConnect)
putBoolean("auto_accept_requests", autoAcceptRequests)
putBoolean("enable_notifications", enableNotifications)
putBoolean("keep_screen_on", keepScreenOn)
putString("preferred_camera", preferredCamera)
putString("stream_quality", streamQuality)
}
onServerConfigSaved(serverUrl)
}
) {
Text(
"Сохранить",
color = GodEyeColors.SuccessGreen,
fontWeight = FontWeight.Medium
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = GodEyeColors.BlackSoft.copy(alpha = 0.9f)
)
)
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Секция "Сервер"
item {
SettingsSection(title = "Подключение к серверу") {
OutlinedTextField(
value = serverUrl,
onValueChange = { serverUrl = it },
label = { Text("URL сервера") },
placeholder = { Text("http://192.168.1.100:3001") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = GodEyeColors.NavyLight,
unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f),
focusedTextColor = GodEyeColors.IvoryPure,
unfocusedTextColor = GodEyeColors.IvorySoft,
focusedLabelColor = GodEyeColors.NavyLight,
unfocusedLabelColor = GodEyeColors.IvorySoft
),
leadingIcon = {
Icon(
Icons.Default.Language,
contentDescription = null,
tint = GodEyeColors.NavyLight
)
}
)
Spacer(modifier = Modifier.height(8.dp))
SettingsSwitchCard(
title = "Автоматическое подключение",
subtitle = "Подключаться к серверу при запуске приложения",
checked = autoConnect,
onCheckedChange = { autoConnect = it },
icon = Icons.Default.AutoAwesome
)
}
}
// Секция "Устройство"
item {
SettingsSection(title = "Устройство") {
OutlinedTextField(
value = deviceName,
onValueChange = { deviceName = it },
label = { Text("Имя устройства") },
placeholder = { Text("Android Device") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = GodEyeColors.NavyLight,
unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f),
focusedTextColor = GodEyeColors.IvoryPure,
unfocusedTextColor = GodEyeColors.IvorySoft,
focusedLabelColor = GodEyeColors.NavyLight,
unfocusedLabelColor = GodEyeColors.IvorySoft
),
leadingIcon = {
Icon(
Icons.Default.Smartphone,
contentDescription = null,
tint = GodEyeColors.NavyLight
)
}
)
Text(
text = "Это имя будет отображаться операторам при подключении",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft,
modifier = Modifier.padding(start = 48.dp, top = 4.dp)
)
}
}
// Секция "Автоматизация"
item {
SettingsSection(title = "Автоматизация") {
SettingsSwitchCard(
title = "Автоматическое принятие запросов",
subtitle = "Автоматически принимать запросы от операторов",
checked = autoAcceptRequests,
onCheckedChange = { autoAcceptRequests = it },
icon = Icons.Default.AutoAwesome
)
Spacer(modifier = Modifier.height(8.dp))
SettingsSwitchCard(
title = "Уведомления",
subtitle = "Показывать уведомления о входящих запросах",
checked = enableNotifications,
onCheckedChange = { enableNotifications = it },
icon = Icons.Default.Notifications
)
Spacer(modifier = Modifier.height(8.dp))
SettingsSwitchCard(
title = "Не выключать экран",
subtitle = "Экран остается включенным во время сессии",
checked = keepScreenOn,
onCheckedChange = { keepScreenOn = it },
icon = Icons.Default.ScreenLockPortrait
)
}
}
// Секция "О приложении"
item {
SettingsSection(title = "О приложении") {
InfoCard(
title = "GodEye Android Client",
subtitle = "Версия 1.0.0 (Build 1)",
icon = Icons.Default.Info
)
Spacer(modifier = Modifier.height(8.dp))
InfoCard(
title = "Device ID",
subtitle = context.getPreferences().getString("device_id", "Неизвестно") ?: "Неизвестно",
icon = Icons.Default.Fingerprint
)
}
}
}
}
}
@Composable
fun SettingsSection(
title: String,
content: @Composable ColumnScope.() -> Unit
) {
Column {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = GodEyeColors.IvoryPure,
modifier = Modifier.padding(bottom = 12.dp)
)
content()
}
}
@Composable
fun SettingsSwitchCard(
title: String,
subtitle: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
icon: androidx.compose.ui.graphics.vector.ImageVector
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (checked) GodEyeColors.SuccessGreen else GodEyeColors.IvorySoft,
modifier = Modifier.size(24.dp)
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
}
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedThumbColor = GodEyeColors.IvoryPure,
checkedTrackColor = GodEyeColors.SuccessGreen,
uncheckedThumbColor = GodEyeColors.IvorySoft,
uncheckedTrackColor = GodEyeColors.NavyDark
)
)
}
}
}
@Composable
fun InfoCard(
title: String,
subtitle: String,
icon: androidx.compose.ui.graphics.vector.ImageVector
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = GodEyeColors.NavyLight,
modifier = Modifier.size(24.dp)
)
Column {
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
}
}
}
}

View File

@@ -1,236 +0,0 @@
package com.example.godeye.camera
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.example.godeye.utils.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
/**
* CameraManager - безопасное управление камерой для предпросмотра
* Исправлены проблемы с освобождением ресурсов и утечками памяти
*/
class CameraManager(private val context: Context) {
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var preview: Preview? = null
private var imageCapture: ImageCapture? = null
private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
private var currentCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var isReleased = false
/**
* Проверка разрешений камеры
*/
fun hasPermissions(): Boolean {
return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
}
/**
* Безопасная настройка камеры с предпросмотром
*/
fun setupCamera(
previewView: PreviewView,
lifecycleOwner: LifecycleOwner,
cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
) {
if (isReleased) {
Logger.error("CAMERA_SETUP_ERROR", "Camera manager already released")
return
}
try {
// Сначала безопасно освобождаем предыдущие ресурсы
safeCameraCleanup()
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProvider = cameraProviderFuture.get()
currentCameraSelector = cameraSelector
// Создание preview use case с безопасными настройками
preview = Preview.Builder()
.setTargetRotation(previewView.display.rotation)
.build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
// Создание image capture use case с оптимизацией для Android 9
imageCapture = ImageCapture.Builder()
.setTargetRotation(previewView.display.rotation)
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
// Отвязка всех use cases перед привязкой новых
cameraProvider?.unbindAll()
// Безопасная привязка use cases к lifecycle
camera = cameraProvider?.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture
)
Logger.step("CAMERA_SETUP", "Camera setup completed safely")
} catch (e: Exception) {
Logger.error("CAMERA_SETUP_ERROR", "Failed to setup camera safely", e)
safeCameraCleanup()
throw e
}
}
/**
* Безопасная очистка ресурсов камеры
*/
private fun safeCameraCleanup() {
try {
cameraProvider?.unbindAll()
preview = null
imageCapture = null
camera = null
} catch (e: Exception) {
Logger.error("CAMERA_CLEANUP_ERROR", "Error during camera cleanup", e)
}
}
/**
* Переключение камеры с безопасной обработкой
*/
fun switchCamera(): CameraSelector {
if (isReleased) return currentCameraSelector
currentCameraSelector = if (currentCameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
return currentCameraSelector
}
/**
* Безопасное включение/выключение вспышки
*/
fun toggleFlash() {
if (isReleased) return
try {
camera?.let { camera ->
if (camera.cameraInfo.hasFlashUnit()) {
val flashMode = camera.cameraInfo.torchState.value
camera.cameraControl.enableTorch(flashMode == TorchState.OFF)
}
}
} catch (e: Exception) {
Logger.error("FLASH_TOGGLE_ERROR", "Error toggling flash", e)
}
}
/**
* Безопасная съемка фото
*/
fun takePhoto(
outputFile: File,
onPhotoTaken: (File) -> Unit,
onError: (Exception) -> Unit
) {
if (isReleased) {
onError(Exception("Camera manager released"))
return
}
val imageCapture = imageCapture ?: run {
onError(Exception("ImageCapture not initialized"))
return
}
try {
val outputOptions = ImageCapture.OutputFileOptions.Builder(outputFile).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
onPhotoTaken(outputFile)
Logger.step("PHOTO_TAKEN", "Photo saved safely to: ${outputFile.absolutePath}")
}
override fun onError(exception: ImageCaptureException) {
onError(exception)
Logger.error("PHOTO_ERROR", "Photo capture failed safely", exception)
}
}
)
} catch (e: Exception) {
onError(e)
Logger.error("PHOTO_SETUP_ERROR", "Failed to setup photo capture", e)
}
}
/**
* Начало записи видео - заглушка для совместимости
*/
fun startRecording(
outputFile: File,
onRecordingStarted: () -> Unit,
onError: (Exception) -> Unit
) {
onError(Exception("Video recording not supported on this device for stability"))
}
/**
* Остановка записи видео - заглушка для совместимости
*/
fun stopRecording() {
Logger.step("VIDEO_RECORDING_STOPPED", "Video recording stop requested (not supported)")
}
/**
* Безопасное освобождение ресурсов
*/
fun release() {
if (isReleased) return
try {
Logger.step("CAMERA_MANAGER_RELEASING", "Starting safe camera manager release")
isReleased = true
// Безопасная очистка камеры
safeCameraCleanup()
// Безопасное завершение executor
cameraExecutor.shutdown()
try {
if (!cameraExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
cameraExecutor.shutdownNow()
}
} catch (e: InterruptedException) {
cameraExecutor.shutdownNow()
Thread.currentThread().interrupt()
}
cameraProvider = null
Logger.step("CAMERA_MANAGER_RELEASED", "Camera manager resources released safely")
} catch (e: Exception) {
Logger.error("CAMERA_RELEASE_ERROR", "Error during camera manager release", e)
}
}
}

View File

@@ -1,315 +0,0 @@
package com.example.godeye.camera
import android.Manifest
import android.content.Context
import android.view.SurfaceView
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.example.godeye.ui.components.*
import com.example.godeye.ui.theme.GodEyeColors
import com.example.godeye.utils.Logger
import kotlinx.coroutines.launch
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@Composable
fun CameraScreen(
onBackPressed: () -> Unit,
sessionId: String = "",
@Suppress("UNUSED_PARAMETER") operatorId: String = ""
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var hasPermissions by remember { mutableStateOf(false) }
var showError by remember { mutableStateOf<String?>(null) }
// Упрощенный CameraManager без сложных анимаций
val cameraManager = remember {
try {
CameraManager(context)
} catch (e: Exception) {
Logger.error("CAMERA_MANAGER_CREATE_ERROR", "Failed to create camera manager", e)
null
}
}
val previewView = remember {
try {
PreviewView(context).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
} catch (e: Exception) {
Logger.error("PREVIEW_VIEW_CREATE_ERROR", "Failed to create preview view", e)
null
}
}
// Проверка разрешений
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
hasPermissions = permissions.values.all { it }
if (!hasPermissions) {
showError = "Необходимы разрешения для работы с камерой"
}
}
LaunchedEffect(Unit) {
hasPermissions = cameraManager?.hasPermissions() ?: false
if (!hasPermissions) {
permissionLauncher.launch(
arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
)
}
}
// Простая инициализация камеры БЕЗ сложных анимаций
LaunchedEffect(hasPermissions) {
if (hasPermissions && cameraManager != null && previewView != null) {
try {
Logger.step("CAMERA_INIT_START", "Starting simple camera initialization")
cameraManager.setupCamera(previewView, lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA)
Logger.step("CAMERA_INIT_SUCCESS", "Simple camera initialized successfully")
} catch (e: Exception) {
showError = "Ошибка инициализации камеры: ${e.message}"
Logger.error("CAMERA_INIT_ERROR", "Camera initialization failed", e)
}
}
}
// Безопасное освобождение ресурсов при закрытии
DisposableEffect(cameraManager) {
onDispose {
try {
Logger.step("CAMERA_SCREEN_DISPOSE", "Disposing camera screen safely")
cameraManager?.release()
} catch (e: Exception) {
Logger.error("CAMERA_DISPOSE_ERROR", "Error disposing camera", e)
}
}
}
// УПРОЩЕННЫЙ UI БЕЗ СЛОЖНЫХ АНИМАЦИЙ
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
if (hasPermissions && previewView != null && cameraManager != null) {
// Простой preview БЕЗ сложных эффектов
AndroidView(
factory = { previewView },
modifier = Modifier.fillMaxSize()
)
// Простая верхняя панель БЕЗ анимаций
Card(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(16.dp)
.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color.Black.copy(alpha = 0.7f)
),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = onBackPressed,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Gray
)
) {
Text("← Назад", color = Color.White)
}
Text(
text = "GodEye Camera",
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.width(60.dp))
}
}
// Простая нижняя панель БЕЗ анимаций
Card(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(16.dp)
.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color.Black.copy(alpha = 0.7f)
),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
// Простая кнопка фото БЕЗ анимаций
Button(
onClick = {
try {
val photoFile = File(
context.externalCacheDir,
"photo_${System.currentTimeMillis()}.jpg"
)
cameraManager.takePhoto(
photoFile,
onPhotoTaken = {
Logger.step("PHOTO_TAKEN", "Photo taken successfully")
},
onError = { error ->
showError = "Ошибка съемки: ${error.message}"
}
)
} catch (e: Exception) {
showError = "Ошибка съемки: ${e.message}"
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Blue
)
) {
Text("📷 Фото", color = Color.White)
}
// Простая кнопка переключения камеры БЕЗ анимаций
Button(
onClick = {
try {
val newCameraSelector = cameraManager.switchCamera()
// Простое пересоздание камеры с новым селектором
cameraManager.setupCamera(previewView, lifecycleOwner, newCameraSelector)
} catch (e: Exception) {
showError = "Ошибка переключения камеры"
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Green
)
) {
Text("🔄 Камера", color = Color.White)
}
}
}
} else {
// Простой экран разрешений БЕЗ анимаций
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = if (cameraManager == null || previewView == null) "Ошибка камеры" else "Требуются разрешения",
style = MaterialTheme.typography.headlineMedium,
color = Color.White,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
if (cameraManager != null && previewView != null) {
Button(
onClick = {
permissionLauncher.launch(
arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
)
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Blue
)
) {
Text("Предоставить разрешения", color = Color.White)
}
} else {
Button(
onClick = onBackPressed,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Red
)
) {
Text("Вернуться назад", color = Color.White)
}
}
}
}
// Простое отображение ошибок БЕЗ анимаций
showError?.let { error ->
Card(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(16.dp)
.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color.Red.copy(alpha = 0.9f)
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = error,
color = Color.White,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { showError = null },
colors = ButtonDefaults.buttonColors(
containerColor = Color.White.copy(alpha = 0.2f)
)
) {
Text("Закрыть", color = Color.White)
}
}
}
}
}
}

View File

@@ -0,0 +1,760 @@
package com.example.godeye.managers
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import com.example.godeye.R
import com.example.godeye.utils.PreferenceManager
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONObject
/**
* Менеджер для автоматической обработки запросов доступа к камере
*/
class AutoApprovalManager(
private val context: Context,
private val preferenceManager: PreferenceManager,
private val connectionManager: ConnectionManager
) {
companion object {
private const val TAG = "AutoApprovalManager"
private const val NOTIFICATION_CHANNEL_ID = "camera_requests"
private const val NOTIFICATION_ID = 1001
}
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Активные сессии камеры с дополнительными метриками для мониторинга
data class CameraSession(
val sessionId: String,
val operatorId: String,
val cameraType: String,
val startTime: Long = System.currentTimeMillis(),
val isAutoApproved: Boolean = false,
// Дополнительные поля для архитектуры GodEye Signal Center
val deviceId: String,
val quality: String = "720p",
var lastHeartbeat: Long = System.currentTimeMillis(),
var streamingState: StreamingState = StreamingState.INITIALIZING
)
enum class StreamingState {
INITIALIZING,
CONNECTING,
STREAMING,
PAUSED,
ERROR,
DISCONNECTED
}
// Состояние системы GodEye Signal Center
private val _systemState = MutableStateFlow(SystemState.READY)
val systemState: StateFlow<SystemState> = _systemState.asStateFlow()
enum class SystemState {
READY,
PROCESSING_REQUEST,
STREAMING_ACTIVE,
ERROR,
MAINTENANCE
}
private val _activeSessions = MutableStateFlow<List<CameraSession>>(emptyList())
val activeSessions: StateFlow<List<CameraSession>> = _activeSessions.asStateFlow()
// Слушатели для UI
private val _pendingRequest = MutableStateFlow<JSONObject?>(null)
val pendingRequest: StateFlow<JSONObject?> = _pendingRequest.asStateFlow()
init {
createNotificationChannel()
setupConnectionManagerListeners()
}
/**
* Настройка слушателей событий от ConnectionManager
* Поддерживает обе конвенции именования: с двоеточиями (desktop/web) и дефисами (android)
*/
private fun setupConnectionManagerListeners() {
Log.d(TAG, "Setting up connection manager listeners with dual naming convention support")
// Обработка запросов доступа к камере (поддержка обеих конвенций)
val cameraRequestHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
try {
Log.d(TAG, "Received camera request event with ${data.size} elements")
if (data.isNotEmpty()) {
val requestData = data[0] as? JSONObject
Log.d(TAG, "Request data: $requestData")
requestData?.let { handleCameraRequest(it) }
} else {
Log.w(TAG, "camera request event received but data is empty")
}
} catch (e: Exception) {
Log.e(TAG, "Error processing camera request event", e)
}
}
// Регистрируем обработчики для обеих конвенций
connectionManager.registerEventListener("camera:request", cameraRequestHandler) // Desktop/Web format
connectionManager.registerEventListener("camera-request", cameraRequestHandler) // Android format
// Обработка автоматически одобренных запросов
val autoApprovedHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
try {
if (data.isNotEmpty()) {
val requestData = data[0] as? JSONObject
requestData?.let { handleAutoApprovedRequest(it) }
}
} catch (e: Exception) {
Log.e(TAG, "Error processing camera auto-approved event", e)
}
}
connectionManager.registerEventListener("camera:auto-approved", autoApprovedHandler)
connectionManager.registerEventListener("camera-auto-approved", autoApprovedHandler)
// Обработка отключения камеры
val disconnectHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
try {
if (data.isNotEmpty()) {
val requestData = data[0] as? JSONObject
requestData?.let { handleCameraDisconnect(it) }
}
} catch (e: Exception) {
Log.e(TAG, "Error processing camera disconnect event", e)
}
}
connectionManager.registerEventListener("camera:disconnect", disconnectHandler)
connectionManager.registerEventListener("camera-disconnect", disconnectHandler)
// Обработка WebRTC answer
val webrtcAnswerHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
try {
if (data.isNotEmpty()) {
val requestData = data[0] as? JSONObject
requestData?.let {
val sessionId = it.optString("sessionId")
val answer = it.optString("answer")
if (sessionId.isNotEmpty() && answer.isNotEmpty()) {
Log.d(TAG, "Received WebRTC answer for session: $sessionId")
connectionManager.handleWebRTCAnswer(sessionId, answer)
} else {
Log.w(TAG, "Invalid WebRTC answer data: sessionId=$sessionId, answer length=${answer.length}")
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error processing webrtc answer event", e)
}
}
connectionManager.registerEventListener("webrtc:answer", webrtcAnswerHandler)
connectionManager.registerEventListener("webrtc-answer", webrtcAnswerHandler)
// Добавляем слушатель для простого "answer" от signaling сервера
connectionManager.registerEventListener("answer", webrtcAnswerHandler)
// Обработка WebRTC ICE candidates
val iceCandidateHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
try {
if (data.isNotEmpty()) {
val requestData = data[0] as? JSONObject
requestData?.let {
val sessionId = it.optString("sessionId")
val candidate = it.optString("candidate")
if (sessionId.isNotEmpty() && candidate.isNotEmpty()) {
Log.d(TAG, "Received ICE candidate for session: $sessionId")
connectionManager.handleWebRTCIceCandidate(sessionId, candidate)
} else {
Log.w(TAG, "Invalid ICE candidate data: sessionId=$sessionId, candidate length=${candidate.length}")
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error processing webrtc ice-candidate event", e)
}
}
connectionManager.registerEventListener("webrtc:ice-candidate", iceCandidateHandler)
connectionManager.registerEventListener("webrtc-ice-candidate", iceCandidateHandler)
connectionManager.registerEventListener("ice:candidate", iceCandidateHandler) // Alternative format
connectionManager.registerEventListener("ice-candidate", iceCandidateHandler) // Alternative format
// Добавляем слушатель для простого "ice_candidate" от signaling сервера
connectionManager.registerEventListener("ice_candidate", iceCandidateHandler)
}
/**
* Обработка запроса доступа к камере
*/
private fun handleCameraRequest(data: JSONObject) {
// Проверяем, что JSON не пустой
if (data.length() == 0) {
Log.e(TAG, "Empty JSON object received for camera request")
return
}
val sessionId = data.optString("sessionId")
val operatorId = data.optString("operatorId")
val cameraType = data.optString("cameraType", "back")
// Расширенная валидация входных данных
when {
sessionId.isBlank() -> {
Log.e(TAG, "Invalid camera request: sessionId is empty or blank")
return
}
operatorId.isBlank() -> {
Log.e(TAG, "Invalid camera request: operatorId is empty or blank")
return
}
cameraType.isBlank() -> {
Log.e(TAG, "Invalid camera request: cameraType is empty or blank, using default 'back'")
// Не возвращаемся, а используем значение по умолчанию
}
}
// Проверяем, нет ли уже активной сессии с этим sessionId
if (_activeSessions.value.any { it.sessionId == sessionId }) {
Log.w(TAG, "Session $sessionId already exists, ignoring duplicate request")
return
}
Log.d(TAG, "Processing camera request - Session: $sessionId, Operator: $operatorId, Camera: $cameraType")
Log.d(TAG, "Auto approve setting: ${preferenceManager.getAutoApprove()}")
if (preferenceManager.getAutoApprove()) {
// Автоматическое подтверждение включено
Log.d(TAG, "Auto approve is ENABLED - automatically approving request")
approveRequest(sessionId, operatorId, cameraType, isAutoApproved = true)
} else {
// Показать запрос пользователю
Log.d(TAG, "Auto approve is DISABLED - showing notification to user")
_pendingRequest.value = data
showRequestNotification(operatorId, cameraType)
}
}
/**
* Подтверждение запроса доступа к камере
*/
fun approveRequest(sessionId: String, operatorId: String, cameraType: String, isAutoApproved: Boolean = false) {
scope.launch {
try {
Log.d(TAG, "Approving camera request - Session: $sessionId")
// Валидация входных данных
if (sessionId.isEmpty() || operatorId.isEmpty()) {
Log.e(TAG, "Cannot approve request: invalid parameters (sessionId=$sessionId, operatorId=$operatorId)")
return@launch
}
// Проверяем, нет ли уже активной сессии с этим sessionId
if (_activeSessions.value.any { it.sessionId == sessionId }) {
Log.w(TAG, "Session $sessionId already exists, cannot approve duplicate")
return@launch
}
// Отправляем подтверждение на сервер
connectionManager.sendCameraResponse(sessionId, true, "", operatorId)
// Добавляем сессию в активные
val session = CameraSession(sessionId, operatorId, cameraType, isAutoApproved = isAutoApproved, deviceId = android.provider.Settings.Secure.getString(
context.contentResolver,
android.provider.Settings.Secure.ANDROID_ID
))
val currentSessions = _activeSessions.value.toMutableList()
currentSessions.add(session)
_activeSessions.value = currentSessions
// Запускаем стриминг через ConnectionManager (не через WebRTC напрямую)
connectionManager.startVideoStream(sessionId, operatorId, cameraType)
// Очищаем pending запрос
_pendingRequest.value = null
// Показываем уведомление
showActiveStreamingNotification(operatorId, cameraType)
Log.d(TAG, "Camera request approved and streaming started for session: $sessionId")
} catch (e: Exception) {
Log.e(TAG, "Failed to approve camera request for session: $sessionId", e)
}
}
}
/**
* Отклонение запроса доступа к камере
*/
fun denyRequest(sessionId: String) {
scope.launch {
try {
Log.d(TAG, "Denying camera request - Session: $sessionId")
// Проверяем, что sessionId не пустой
if (sessionId.isEmpty()) {
Log.e(TAG, "Cannot deny request: sessionId is empty")
return@launch
}
// Получаем информацию о запросе из pending request
val pendingRequestData = _pendingRequest.value
val operatorId = pendingRequestData?.optString("operatorId") ?: ""
// Отправляем отклонение на сервер
connectionManager.sendCameraResponse(sessionId, false, "User denied request", operatorId)
// Очищаем pending запрос
_pendingRequest.value = null
Log.d(TAG, "Camera request denied for session: $sessionId")
} catch (e: Exception) {
Log.e(TAG, "Failed to deny camera request for session: $sessionId", e)
}
}
}
/**
* Обработка отключения камеры
*/
private fun handleCameraDisconnect(data: JSONObject) {
val sessionId = data.optString("sessionId")
Log.d(TAG, "Disconnecting camera session: $sessionId")
// Остановка WebRTC стриминга через ConnectionManager
connectionManager.stopVideoStream(sessionId)
// Удаление сессии из активных
val currentSessions = _activeSessions.value.toMutableList()
currentSessions.removeAll { it.sessionId == sessionId }
_activeSessions.value = currentSessions
// Обновление уведомления
if (currentSessions.isEmpty()) {
notificationManager.cancel(NOTIFICATION_ID)
} else {
updateActiveStreamingNotification(currentSessions)
}
}
/**
* Создание канала уведомлений
*/
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"Запросы доступа к камере",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Уведомления о запросах операторов на доступ к камере"
}
notificationManager.createNotificationChannel(channel)
}
}
/**
* Показать уведомление о запросе
*/
private fun showRequestNotification(operatorId: String, cameraType: String) {
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_camera_request)
.setContentTitle("Запрос доступа к камере")
.setContentText("Оператор $operatorId запрашивает доступ к ${getCameraDisplayName(cameraType)}")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(false)
.setStyle(NotificationCompat.BigTextStyle()
.bigText("Оператор с UUID: $operatorId запрашивает доступ к ${getCameraDisplayName(cameraType)}"))
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
/**
* Показать уведомление об активном стриминге
*/
private fun showActiveStreamingNotification(operatorId: String, cameraType: String) {
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_camera_active)
.setContentTitle("Камера активна")
.setContentText("Оператор $operatorId подключен к ${getCameraDisplayName(cameraType)}")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setOngoing(true)
.setStyle(NotificationCompat.BigTextStyle()
.bigText("Оператор с UUID: $operatorId подключен к ${getCameraDisplayName(cameraType)}"))
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
private fun updateActiveStreamingNotification(sessions: List<CameraSession>) {
val operatorIds = sessions.map { it.operatorId }.distinct()
val operatorText = if (operatorIds.size == 1) {
"UUID: ${operatorIds.first()}"
} else {
"Операторы: ${operatorIds.size} подключено"
}
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_camera_active)
.setContentTitle("Камера активна")
.setContentText(operatorText)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setOngoing(true)
.setStyle(NotificationCompat.BigTextStyle()
.bigText("Активные операторы:\n${operatorIds.joinToString("\n") { "UUID: $it" }}"))
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
private fun getCameraDisplayName(cameraType: String): String {
return when (cameraType) {
"back" -> "основной камере"
"front" -> "фронтальной камере"
"wide" -> "широкоугольной камере"
"telephoto" -> "телеобъективу"
else -> "камере"
}
}
/**
* Обработка автоматически одобренного запроса
*/
private fun handleAutoApprovedRequest(data: JSONObject) {
val sessionId = data.optString("sessionId")
val operatorId = data.optString("operatorId")
val cameraType = data.optString("cameraType", "back")
approveRequest(sessionId, operatorId, cameraType, isAutoApproved = true)
}
/**
* Принудительное отключение сессии
*/
fun disconnectSession(sessionId: String) {
scope.launch {
try {
Log.d(TAG, "Forcibly disconnecting session: $sessionId")
// Остановка видеопотока
connectionManager.stopVideoStream(sessionId)
// Удаление из активных сессий
val currentSessions = _activeSessions.value.toMutableList()
val removedSession = currentSessions.find { it.sessionId == sessionId }
currentSessions.removeAll { it.sessionId == sessionId }
_activeSessions.value = currentSessions
// Обновление уведомлений
if (currentSessions.isEmpty()) {
notificationManager.cancel(NOTIFICATION_ID)
} else {
updateActiveStreamingNotification(currentSessions)
}
Log.d(TAG, "Session $sessionId disconnected successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to disconnect session: $sessionId", e)
}
}
}
/**
* Отключение всех активных сессий
*/
fun disconnectAllSessions() {
scope.launch {
try {
Log.d(TAG, "Disconnecting all active sessions")
val currentSessions = _activeSessions.value.toList()
// Остановка всех видеопотоков
currentSessions.forEach { session ->
connectionManager.stopVideoStream(session.sessionId)
}
// Очистка активных сессий
_activeSessions.value = emptyList()
// Отмена уведомлений
notificationManager.cancel(NOTIFICATION_ID)
Log.d(TAG, "All sessions disconnected (${currentSessions.size} sessions)")
} catch (e: Exception) {
Log.e(TAG, "Failed to disconnect all sessions", e)
}
}
}
/**
* Получение информации о сессии
*/
fun getSessionInfo(sessionId: String): CameraSession? {
return _activeSessions.value.find { it.sessionId == sessionId }
}
/**
* Получение количества активных сессий
*/
fun getActiveSessionCount(): Int {
return _activeSessions.value.size
}
/**
* Проверка, есть ли активная сессия с данным оператором
*/
fun hasActiveSessionWithOperator(operatorId: String): Boolean {
return _activeSessions.value.any { it.operatorId == operatorId }
}
/**
* Очистка ресурсов
*/
fun cleanup() {
scope.cancel()
notificationManager.cancel(NOTIFICATION_ID)
// Очистка активных сессий при закрытии
_activeSessions.value = emptyList()
_pendingRequest.value = null
}
/**
* Обновление состояния потоковой передачи сессии
*/
fun updateSessionStreamingState(sessionId: String, state: StreamingState) {
scope.launch {
val currentSessions = _activeSessions.value.toMutableList()
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
if (sessionIndex != -1) {
val updatedSession = currentSessions[sessionIndex].copy(
streamingState = state,
lastHeartbeat = System.currentTimeMillis()
)
currentSessions[sessionIndex] = updatedSession
_activeSessions.value = currentSessions
Log.d(TAG, "Updated session $sessionId state to $state")
// Обновляем общее состояние системы
updateSystemState()
}
}
}
/**
* Обновление общего состояния системы GodEye Signal Center
*/
private fun updateSystemState() {
val sessions = _activeSessions.value
val newState = when {
sessions.isEmpty() -> SystemState.READY
sessions.any { it.streamingState == StreamingState.ERROR } -> SystemState.ERROR
sessions.any { it.streamingState == StreamingState.STREAMING } -> SystemState.STREAMING_ACTIVE
sessions.all { it.streamingState == StreamingState.INITIALIZING || it.streamingState == StreamingState.CONNECTING } ->
SystemState.PROCESSING_REQUEST
else -> SystemState.READY
}
if (_systemState.value != newState) {
_systemState.value = newState
Log.d(TAG, "System state updated to: $newState")
}
}
/**
* Получение диагностической информации для архитектуры GodEye Signal Center
*/
fun getDiagnosticInfo(): JSONObject {
return JSONObject().apply {
put("systemState", _systemState.value.name)
put("activeSessions", _activeSessions.value.size)
put("autoApproveEnabled", preferenceManager.getAutoApprove())
put("timestamp", System.currentTimeMillis())
// Детальная информация о сессиях
val sessionsArray = org.json.JSONArray()
_activeSessions.value.forEach { session ->
val sessionInfo = JSONObject().apply {
put("sessionId", session.sessionId)
put("operatorId", session.operatorId)
put("cameraType", session.cameraType)
put("isAutoApproved", session.isAutoApproved)
put("streamingState", session.streamingState.name)
put("duration", System.currentTimeMillis() - session.startTime)
put("lastHeartbeat", session.lastHeartbeat)
put("deviceId", session.deviceId)
put("quality", session.quality)
}
sessionsArray.put(sessionInfo)
}
put("sessions", sessionsArray)
// Статистика для мониторинга
val stats = JSONObject().apply {
val streamingCount = _activeSessions.value.count { it.streamingState == StreamingState.STREAMING }
val errorCount = _activeSessions.value.count { it.streamingState == StreamingState.ERROR }
val connectingCount = _activeSessions.value.count { it.streamingState == StreamingState.CONNECTING }
put("streamingSessions", streamingCount)
put("errorSessions", errorCount)
put("connectingSessions", connectingCount)
put("autoApprovedSessions", _activeSessions.value.count { it.isAutoApproved })
}
put("statistics", stats)
}
}
/**
* Проверка "мертвых" сессий и их очистка
* Метод для поддержания надежности системы GodEye Signal Center
*/
fun performHealthCheck() {
scope.launch {
val currentTime = System.currentTimeMillis()
val staleThreshold = 30000L // 30 секунд без heartbeat
val currentSessions = _activeSessions.value.toMutableList()
val staleSessions = currentSessions.filter { session ->
currentTime - session.lastHeartbeat > staleThreshold &&
session.streamingState != StreamingState.STREAMING
}
if (staleSessions.isNotEmpty()) {
Log.w(TAG, "Found ${staleSessions.size} stale sessions, cleaning up...")
staleSessions.forEach { session ->
Log.w(TAG, "Removing stale session: ${session.sessionId}")
connectionManager.stopVideoStream(session.sessionId)
currentSessions.remove(session)
}
_activeSessions.value = currentSessions
updateSystemState()
// Обновляем уведомления
if (currentSessions.isEmpty()) {
notificationManager.cancel(NOTIFICATION_ID)
} else {
updateActiveStreamingNotification(currentSessions)
}
}
}
}
/**
* Отправка heartbeat для активных сессий
* Поддерживает мониторинг состояния в архитектуре GodEye Signal Center
*/
fun sendSessionHeartbeat(sessionId: String) {
scope.launch {
val currentSessions = _activeSessions.value.toMutableList()
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
if (sessionIndex != -1) {
val updatedSession = currentSessions[sessionIndex].copy(
lastHeartbeat = System.currentTimeMillis()
)
currentSessions[sessionIndex] = updatedSession
_activeSessions.value = currentSessions
// Отправляем heartbeat на сервер через ConnectionManager
try {
val heartbeatData = JSONObject().apply {
put("sessionId", sessionId)
put("timestamp", System.currentTimeMillis())
put("streamingState", updatedSession.streamingState.name)
put("deviceId", updatedSession.deviceId)
}
// Используем методы ConnectionManager для отправки heartbeat
// Пока не создадим специальный метод, логируем для отладки
Log.d(TAG, "Sending session heartbeat for $sessionId: $heartbeatData")
} catch (e: Exception) {
Log.e(TAG, "Failed to send heartbeat for session $sessionId", e)
}
}
}
}
/**
* Получение метрик производительности для мониторинга GodEye Signal Center
*/
fun getPerformanceMetrics(): JSONObject {
val sessions = _activeSessions.value
val currentTime = System.currentTimeMillis()
return JSONObject().apply {
// Общие метрики
put("totalSessions", sessions.size)
put("systemUptime", currentTime - startTime)
put("averageSessionDuration", if (sessions.isNotEmpty()) {
sessions.map { currentTime - it.startTime }.average()
} else 0.0)
// Метрики по состоянию
StreamingState.values().forEach { state ->
put("sessions_${state.name.lowercase()}", sessions.count { it.streamingState == state })
}
// Метрики по типам камер
val cameraTypes = sessions.groupBy { it.cameraType }
cameraTypes.forEach { (type, typeSessions) ->
put("camera_${type}_sessions", typeSessions.size)
}
// Метрики автоодобрения
put("autoApprovedSessions", sessions.count { it.isAutoApproved })
put("manualApprovedSessions", sessions.count { !it.isAutoApproved })
put("timestamp", currentTime)
}
}
private val startTime = System.currentTimeMillis()
/**
* Генерация UUID для новой сессии
*/
fun generateSessionId(): String {
return java.util.UUID.randomUUID().toString()
}
/**
* Тестовая функция для проверки автоподтверждения с UUID
*/
fun testAutoApprovalWithUUID() {
try {
val testSessionId = generateSessionId()
val testOperatorId = java.util.UUID.randomUUID().toString()
Log.d(TAG, "Testing auto approval with UUID session ID: $testSessionId")
// Создаем фиктивный запрос камеры с UUID
val testRequest = JSONObject().apply {
put("sessionId", testSessionId)
put("operatorId", testOperatorId)
put("cameraType", "back")
put("timestamp", System.currentTimeMillis())
}
Log.d(TAG, "Simulating camera request with UUID: $testRequest")
// Обрабатываем запрос
handleCameraRequest(testRequest)
} catch (e: Exception) {
Log.e(TAG, "Failed to test auto approval with UUID", e)
}
}
}

View File

@@ -193,6 +193,7 @@ class Camera2Manager(private val context: Context) {
}
}
// Используем стандартный метод createCaptureSession вместо устаревшего
camera.createCaptureSession(listOf(surface), sessionCallback, null)
} catch (e: CameraAccessException) {

File diff suppressed because it is too large Load Diff

View File

@@ -34,10 +34,10 @@ class PermissionManager(private val context: Context) {
Manifest.permission.WAKE_LOCK,
Manifest.permission.FOREGROUND_SERVICE
).apply {
// Добавляем FOREGROUND_SERVICE_CAMERA для API 34+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
plus(Manifest.permission.FOREGROUND_SERVICE_CAMERA)
}
// Добавляем FOREGROUND_SERVICE_CAMERA для API 34+ (но в Android 10 недоступно)
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// plus(Manifest.permission.FOREGROUND_SERVICE_CAMERA)
// }
}
/**
@@ -193,7 +193,7 @@ class PermissionManager(private val context: Context) {
Manifest.permission.ACCESS_NETWORK_STATE -> "Проверка состояния сети"
Manifest.permission.WAKE_LOCK -> "Предотвращение засыпания устройства"
Manifest.permission.FOREGROUND_SERVICE -> "Работа в фоновом режиме"
Manifest.permission.FOREGROUND_SERVICE_CAMERA -> "Фоновая работа с камерой"
// Manifest.permission.FOREGROUND_SERVICE_CAMERA -> "Фоновая работа с камерой" // Недоступно в Android 10
else -> "Системное разрешение"
}
}

View File

@@ -1,156 +1,35 @@
package com.example.godeye.managers
import com.example.godeye.models.*
import com.example.godeye.utils.Logger
import com.example.godeye.models.CameraSession
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.concurrent.ConcurrentHashMap
/**
* SessionManager - управление активными сессиями с операторами
* Отслеживает состояние WebRTC соединений и сессий камеры
*/
class SessionManager {
private val activeSessions = ConcurrentHashMap<String, CameraSession>()
private val _sessions = MutableStateFlow<Map<String, CameraSession>>(emptyMap())
val sessions: StateFlow<Map<String, CameraSession>> = _sessions.asStateFlow()
private val _activeSessionCount = MutableStateFlow(0)
val activeSessionCount: StateFlow<Int> = _activeSessionCount.asStateFlow()
/**
* Создание новой сессии при принятии запроса оператора
*/
fun createSession(sessionId: String, operatorId: String, cameraType: String): CameraSession {
Logger.step("SESSION_CREATE", "Creating session: $sessionId for operator $operatorId")
fun createSession(sessionId: String, operatorId: String, cameraType: String) {
val session = CameraSession(
sessionId = sessionId,
operatorId = operatorId,
cameraType = cameraType,
startTime = System.currentTimeMillis(),
isActive = true,
webRTCConnected = false
startTime = System.currentTimeMillis()
)
activeSessions[sessionId] = session
updateSessionsFlow()
Logger.step("SESSION_CREATED", "Session created: $sessionId")
return session
val currentSessions = _sessions.value.toMutableMap()
currentSessions[sessionId] = session
_sessions.value = currentSessions
}
/**
* Обновление статуса WebRTC соединения для сессии
*/
fun updateWebRTCConnection(sessionId: String, connected: Boolean) {
activeSessions[sessionId]?.let { session ->
session.webRTCConnected = connected
activeSessions[sessionId] = session
updateSessionsFlow()
Logger.step("SESSION_WEBRTC_UPDATED",
"Session $sessionId WebRTC status updated: $connected")
}
fun endSession(sessionId: String, reason: String = "Session ended") {
val currentSessions = _sessions.value.toMutableMap()
currentSessions.remove(sessionId)
_sessions.value = currentSessions
}
/**
* Завершение сессии
*/
fun endSession(sessionId: String, reason: String = "User ended") {
activeSessions[sessionId]?.let { session ->
session.isActive = false
activeSessions.remove(sessionId)
updateSessionsFlow()
Logger.step("SESSION_ENDED", "Session ended: $sessionId, reason: $reason")
}
}
/**
* Получение активной сессии по ID
*/
fun getSession(sessionId: String): CameraSession? {
return activeSessions[sessionId]
}
/**
* Получение всех активных сессий
*/
fun getAllActiveSessions(): List<CameraSession> {
return activeSessions.values.filter { it.isActive }
}
/**
* Проверка, есть ли активные сессии
*/
fun hasActiveSessions(): Boolean {
return activeSessions.values.any { it.isActive }
}
/**
* Завершение всех активных сессий
*/
fun endAllSessions(reason: String = "Service stopped") {
Logger.step("SESSION_END_ALL", "Ending all active sessions: $reason")
activeSessions.values.forEach { session ->
if (session.isActive) {
session.isActive = false
Logger.step("SESSION_ENDED", "Session ended: ${session.sessionId}")
}
}
activeSessions.clear()
updateSessionsFlow()
}
/**
* Переключение камеры для сессии
*/
fun switchCamera(sessionId: String, newCameraType: String) {
activeSessions[sessionId]?.let { session ->
session.cameraType = newCameraType
activeSessions[sessionId] = session
updateSessionsFlow()
Logger.step("SESSION_CAMERA_SWITCHED",
"Session $sessionId camera switched to: $newCameraType")
}
}
/**
* Получение статистики сессий
*/
fun getSessionStats(): SessionStats {
val active = activeSessions.values.filter { it.isActive }
val withWebRTC = active.filter { it.webRTCConnected }
return SessionStats(
totalActive = active.size,
webRTCConnected = withWebRTC.size,
operators = active.map { it.operatorId }.distinct().size,
averageDuration = if (active.isNotEmpty()) {
active.map { System.currentTimeMillis() - it.startTime }.average().toLong()
} else 0L
)
}
private fun updateSessionsFlow() {
_sessions.value = activeSessions.toMap()
_activeSessionCount.value = activeSessions.values.count { it.isActive }
fun endAllSessions(reason: String = "All sessions ended") {
_sessions.value = emptyMap()
}
}
/**
* Статистика сессий
*/
data class SessionStats(
val totalActive: Int,
val webRTCConnected: Int,
val operators: Int,
val averageDuration: Long
)

View File

@@ -0,0 +1,269 @@
package com.example.godeye.managers
import android.content.Context
import android.provider.Settings
import android.util.Log
import com.example.godeye.models.*
import com.example.godeye.signaling.SignalingClient
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* Менеджер сигналлинга для управления подключениями к операторам
*/
class SignalingManager(
private val context: Context
) {
companion object {
private const val TAG = "SignalingManager"
}
private val signalingClient = SignalingClient()
private var managerScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
// Состояния
val signalingState: StateFlow<SignalingState> = signalingClient.signalingState
val sessionState: StateFlow<SessionState> = signalingClient.sessionState
val currentCall: StateFlow<IncomingCall?> = signalingClient.currentCall
// Callbacks для WebRTC интеграции
private var webRTCManager: Any? = null // Будет связан с WebRTCManager
/**
* Запуск сигналлинга
*/
fun start(serverUrl: String? = null) {
Log.d(TAG, "Starting signaling manager")
managerScope.launch {
try {
val deviceInfo = createDeviceInfo()
val url = serverUrl ?: getDefaultServerUrl()
Log.d(TAG, "Connecting to signaling server: $url")
signalingClient.connect(url, deviceInfo)
// Настраиваем обработчики WebRTC сигналлинга
setupWebRTCHandlers()
} catch (e: Exception) {
Log.e(TAG, "Failed to start signaling", e)
}
}
}
/**
* Остановка сигналлинга
*/
fun stop() {
Log.d(TAG, "Stopping signaling manager")
signalingClient.disconnect()
managerScope.cancel()
managerScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
}
/**
* Принятие входящего вызова
*/
fun acceptIncomingCall() {
Log.d(TAG, "Accepting incoming call")
signalingClient.acceptCall()
}
/**
* Отклонение входящего вызова
*/
fun rejectIncomingCall() {
Log.d(TAG, "Rejecting incoming call")
signalingClient.rejectCall()
}
/**
* Завершение активного вызова
*/
fun endActiveCall() {
Log.d(TAG, "Ending active call")
signalingClient.endCall()
}
/**
* Создание информации об устройстве
*/
private fun createDeviceInfo(): DeviceInfo {
return DeviceInfo(
deviceId = getDeviceId(),
deviceName = android.os.Build.MODEL,
androidVersion = android.os.Build.VERSION.RELEASE,
appVersion = "1.0"
)
}
/**
* Получение уникального ID устройства
*/
private fun getDeviceId(): String {
return try {
Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
) ?: "unknown_device_${System.currentTimeMillis()}"
} catch (e: Exception) {
"unknown_device_${System.currentTimeMillis()}"
}
}
/**
* Получение имени устройства
*/
private fun getDeviceName(): String {
return try {
"${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}"
} catch (e: Exception) {
"Android Device"
}
}
/**
* Получение URL сервера по умолчанию
*/
private fun getDefaultServerUrl(): String {
// ИСПРАВЛЕНИЕ: Обновляем IP по умолчанию на ваш сервер
return "ws://192.168.219.108:3000"
}
/**
* Настройка обработчиков WebRTC сигналлинга
*/
private fun setupWebRTCHandlers() {
signalingClient.setOnOfferReceived { offer ->
Log.d(TAG, "Received WebRTC offer")
// TODO: Передать offer в WebRTCManager
handleWebRTCOffer(offer)
}
signalingClient.setOnAnswerReceived { answer ->
Log.d(TAG, "Received WebRTC answer")
// TODO: Передать answer в WebRTCManager
handleWebRTCAnswer(answer)
}
signalingClient.setOnIceCandidateReceived { candidate ->
Log.d(TAG, "Received ICE candidate")
// TODO: Передать candidate в WebRTCManager
handleICECandidate(candidate)
}
signalingClient.setOnCallEnded {
Log.d(TAG, "Call ended by operator")
handleCallEnded()
}
}
/**
* Обработка WebRTC Offer
*/
private fun handleWebRTCOffer(offer: RTCSessionDescription) {
managerScope.launch {
try {
// TODO: Интеграция с WebRTCManager
Log.d(TAG, "Processing WebRTC offer: ${offer.type}")
// Здесь должна быть логика создания answer и его отправки
// val answer = webRTCManager.createAnswer(offer)
// signalingClient.sendAnswer(answer, currentCall.value?.operatorId ?: "")
} catch (e: Exception) {
Log.e(TAG, "Error processing WebRTC offer", e)
}
}
}
/**
* Обработка WebRTC Answer
*/
private fun handleWebRTCAnswer(answer: RTCSessionDescription) {
managerScope.launch {
try {
// TODO: Интеграция с WebRTCManager
Log.d(TAG, "Processing WebRTC answer: ${answer.type}")
// webRTCManager.setRemoteDescription(answer)
} catch (e: Exception) {
Log.e(TAG, "Error processing WebRTC answer", e)
}
}
}
/**
* Обработка ICE Candidate
*/
private fun handleICECandidate(candidate: RTCIceCandidate) {
managerScope.launch {
try {
// TODO: Интеграция с WebRTCManager
Log.d(TAG, "Processing ICE candidate")
// webRTCManager.addIceCandidate(candidate)
} catch (e: Exception) {
Log.e(TAG, "Error processing ICE candidate", e)
}
}
}
/**
* Обработка завершения вызова
*/
private fun handleCallEnded() {
managerScope.launch {
try {
// TODO: Очистка WebRTC соединения
Log.d(TAG, "Cleaning up after call end")
// webRTCManager.cleanup()
} catch (e: Exception) {
Log.e(TAG, "Error cleaning up after call end", e)
}
}
}
/**
* Отправка WebRTC Offer (для исходящих вызовов)
*/
fun sendOffer(offer: RTCSessionDescription) {
val operatorId = currentCall.value?.operatorId ?: return
signalingClient.sendOffer(offer, operatorId)
}
/**
* Отправка WebRTC Answer
*/
fun sendAnswer(answer: RTCSessionDescription) {
val operatorId = currentCall.value?.operatorId ?: return
signalingClient.sendAnswer(answer, operatorId)
}
/**
* Отправка ICE Candidate
*/
fun sendIceCandidate(candidate: RTCIceCandidate) {
val operatorId = currentCall.value?.operatorId ?: return
signalingClient.sendIceCandidate(candidate, operatorId)
}
/**
* Получение информации о текущем состоянии
*/
fun getConnectionInfo(): Map<String, Any> {
return mapOf(
"signalingState" to signalingState.value.name,
"sessionState" to sessionState.value.name,
"hasActiveCall" to (currentCall.value != null),
"operatorId" to (currentCall.value?.operatorId ?: "none"),
"deviceId" to getDeviceId()
)
}
}

View File

@@ -0,0 +1,308 @@
package com.example.godeye.managers
import android.content.Context
import com.example.godeye.utils.Logger
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import io.socket.client.IO
import io.socket.client.Socket
import org.json.JSONObject
import java.net.URI
/**
* Менеджер Socket.IO соединения с сервером
*/
class SocketManager(private val context: Context) {
companion object {
private const val TAG = "SocketManager"
}
// События подключения
sealed class ConnectionEvent {
object Connected : ConnectionEvent()
object Disconnected : ConnectionEvent()
data class Error(val message: String) : ConnectionEvent()
data class CameraRequest(val sessionId: String, val operatorId: String, val message: String) : ConnectionEvent()
}
private val _events = MutableStateFlow<ConnectionEvent?>(null)
val events: Flow<ConnectionEvent?> = _events
private var socket: Socket? = null
private var webRTCManager: WebRTCManager? = null
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
// ИСПРАВЛЕНИЕ: Добавляем флаг для предотвращения множественной регистрации
private var isDeviceRegistered = false
private var currentDeviceId: String? = null
/**
* Подключение к серверу
*/
fun connect(serverUrl: String, webRTCManager: WebRTCManager) {
try {
this.webRTCManager = webRTCManager
Logger.step("SOCKET_CONNECTING", "Connecting to server: $serverUrl")
val options = IO.Options().apply {
forceNew = true
reconnection = true
timeout = 10000
}
socket = IO.socket(URI.create(serverUrl), options)
setupConnectionHandlers()
setupCameraHandlers()
setupWebRTCHandlers()
setupWebRTCCallbacks()
socket?.connect()
} catch (e: Exception) {
Logger.error("SOCKET_CONNECT_ERROR", "Failed to connect to server", e)
_events.value = ConnectionEvent.Error("Ошибка подключения: ${e.message}")
}
}
/**
* Настройка основных обработчиков подключения
*/
private fun setupConnectionHandlers() {
socket?.on(Socket.EVENT_CONNECT) {
Logger.step("SOCKET_CONNECTED", "Connected to server")
_events.value = ConnectionEvent.Connected
}
socket?.on(Socket.EVENT_DISCONNECT) {
Logger.step("SOCKET_DISCONNECTED", "Disconnected from server")
_events.value = ConnectionEvent.Disconnected
}
socket?.on(Socket.EVENT_CONNECT_ERROR) { args ->
val error = if (args.isNotEmpty()) args[0].toString() else "Unknown error"
Logger.error("SOCKET_CONNECT_ERROR", "Connection error: $error", null)
_events.value = ConnectionEvent.Error("Ошибка подключения: $error")
}
}
/**
* Настройка обработчиков камеры
*/
private fun setupCameraHandlers() {
socket?.on("camera_request") { args ->
try {
val data = args[0] as JSONObject
val sessionId = data.getString("sessionId")
val operatorId = data.getString("operatorId")
val message = data.getString("message")
Logger.step("SOCKET_CAMERA_REQUEST", "Camera request received from operator: $operatorId")
_events.value = ConnectionEvent.CameraRequest(sessionId, operatorId, message)
} catch (e: Exception) {
Logger.error("SOCKET_CAMERA_REQUEST_ERROR", "Failed to handle camera request", e)
}
}
socket?.on("stop_camera") { args ->
try {
val data = args[0] as JSONObject
val sessionId = data.getString("sessionId")
Logger.step("SOCKET_STOP_CAMERA", "Stop camera request for session: $sessionId")
webRTCManager?.stopStreaming(sessionId)
} catch (e: Exception) {
Logger.error("SOCKET_STOP_CAMERA_ERROR", "Failed to handle stop camera", e)
}
}
socket?.on("switch_camera") { args ->
try {
val data = args[0] as JSONObject
val sessionId = data.getString("sessionId")
Logger.step("SOCKET_SWITCH_CAMERA", "Switch camera request for session: $sessionId")
webRTCManager?.switchCamera()
} catch (e: Exception) {
Logger.error("SOCKET_SWITCH_CAMERA_ERROR", "Failed to handle switch camera", e)
}
}
}
/**
* Настройка обработчиков WebRTC сигналов
*/
private fun setupWebRTCHandlers() {
socket?.on("webrtc_answer") { args ->
try {
val data = args[0] as JSONObject
val sessionId = data.getString("sessionId")
val answerSdp = data.getString("answer")
Logger.step("SOCKET_WEBRTC_ANSWER", "Received WebRTC answer for session: $sessionId")
webRTCManager?.handleAnswer(sessionId, answerSdp)
} catch (e: Exception) {
Logger.error("SOCKET_WEBRTC_ANSWER_ERROR", "Failed to handle WebRTC answer", e)
}
}
socket?.on("webrtc_ice_candidate") { args ->
try {
val data = args[0] as JSONObject
val sessionId = data.getString("sessionId")
val candidateJson = data.getString("candidate")
Logger.step("SOCKET_WEBRTC_ICE", "Received ICE candidate for session: $sessionId")
webRTCManager?.handleIceCandidate(sessionId, candidateJson)
} catch (e: Exception) {
Logger.error("SOCKET_WEBRTC_ICE_ERROR", "Failed to handle ICE candidate", e)
}
}
}
/**
* Настройка колбэков WebRTC для отправки сигналов на сервер
*/
private fun setupWebRTCCallbacks() {
webRTCManager?.onOfferCreated = { sessionId, offer ->
scope.launch {
try {
// ИСПРАВЛЕНО: Отправляем offer в правильном формате для вашего сервера
val offerData = JSONObject().apply {
put("sessionId", sessionId)
put("offer", offer) // Отправляем как строку SDP, не JSON объект
}
socket?.emit("webrtc_offer", offerData)
Logger.step("SOCKET_WEBRTC_OFFER_SENT", "WebRTC offer sent for session: $sessionId")
} catch (e: Exception) {
Logger.error("SOCKET_WEBRTC_OFFER_ERROR", "Failed to send WebRTC offer", e)
}
}
}
webRTCManager?.onIceCandidateCreated = { sessionId, candidate ->
scope.launch {
try {
val candidateData = JSONObject().apply {
put("sessionId", sessionId)
put("candidate", candidate)
}
socket?.emit("webrtc_ice_candidate", candidateData)
Logger.step("SOCKET_ICE_SENT", "ICE candidate sent for session: $sessionId")
} catch (e: Exception) {
Logger.error("SOCKET_ICE_ERROR", "Failed to send ICE candidate", e)
}
}
}
}
/**
* Регистрация устройства на сервере
*/
fun registerDevice(deviceId: String, deviceName: String) {
try {
// ИСПРАВЛЕНИЕ: Проверяем, зарегистрировано ли уже устройство
if (isDeviceRegistered && deviceId == currentDeviceId) {
Logger.step("SOCKET_DEVICE_ALREADY_REGISTERED", "Device is already registered: $deviceName ($deviceId)")
return
}
val data = JSONObject().apply {
put("deviceId", deviceId)
put("deviceName", deviceName)
}
socket?.emit("device_register", data)
Logger.step("SOCKET_DEVICE_REGISTER", "Device registration sent: $deviceName ($deviceId)")
// ИСПРАВЛЕНИЕ: Устанавливаем флаг регистрации устройства
isDeviceRegistered = true
currentDeviceId = deviceId
} catch (e: Exception) {
Logger.error("SOCKET_REGISTER_ERROR", "Failed to register device", e)
}
}
/**
* Подтверждение доступа к камере
*/
fun approveCameraAccess(sessionId: String) {
try {
val data = JSONObject().apply {
put("sessionId", sessionId)
put("approved", true)
}
socket?.emit("camera_approved", data)
Logger.step("SOCKET_CAMERA_APPROVED", "Camera access approved for session: $sessionId")
} catch (e: Exception) {
Logger.error("SOCKET_APPROVE_ERROR", "Failed to approve camera access", e)
}
}
/**
* Уведомление о запуске камеры
*/
fun notifyCameraStarted(sessionId: String) {
try {
val data = JSONObject().apply {
put("sessionId", sessionId)
}
socket?.emit("camera_started", data)
Logger.step("SOCKET_CAMERA_STARTED", "Camera started notification sent for session: $sessionId")
} catch (e: Exception) {
Logger.error("SOCKET_CAMERA_STARTED_ERROR", "Failed to send camera started notification", e)
}
}
/**
* Отключение от сервера
*/
fun disconnect() {
try {
Logger.step("SOCKET_DISCONNECTING", "Disconnecting from server")
socket?.disconnect()
socket = null
} catch (e: Exception) {
Logger.error("SOCKET_DISCONNECT_ERROR", "Error during disconnect", e)
}
}
/**
* Проверка состояния подключения
*/
fun isConnected(): Boolean {
return socket?.connected() == true
}
/**
* Освобождение ресурсов
*/
fun dispose() {
try {
disconnect()
scope.cancel()
Logger.step("SOCKET_DISPOSED", "SocketManager disposed")
} catch (e: Exception) {
Logger.error("SOCKET_DISPOSE_ERROR", "Error during dispose", e)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,270 @@
package com.example.godeye.managers
import com.example.godeye.utils.Logger
import kotlinx.coroutines.*
import okhttp3.*
import org.json.JSONObject
import java.util.concurrent.TimeUnit
/**
* Менеджер WebSocket соединений для сигналинга WebRTC
*/
class WebSocketManager {
companion object {
private const val TAG = "WebSocketManager"
private const val PING_INTERVAL = 30L // секунд
private const val RECONNECT_INTERVAL = 5L // секунд
private const val MAX_RECONNECT_ATTEMPTS = 5
}
private var webSocket: WebSocket? = null
private var client: OkHttpClient? = null
private var isConnected = false
private var reconnectAttempts = 0
private var shouldReconnect = true
private var currentUrl: String? = null
// Колбэки
var onMessageReceived: ((String) -> Unit)? = null
var onConnectionStateChanged: ((Boolean) -> Unit)? = null
var onError: ((String) -> Unit)? = null
// Корутины
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var reconnectJob: Job? = null
init {
initializeClient()
}
/**
* Инициализация HTTP клиента
*/
private fun initializeClient() {
client = OkHttpClient.Builder()
.pingInterval(PING_INTERVAL, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
}
/**
* Подключение к WebSocket серверу
*/
fun connect(url: String) {
try {
Logger.step("WEBSOCKET_CONNECT", "Connecting to WebSocket: $url")
currentUrl = url
shouldReconnect = true
reconnectAttempts = 0
disconnect() // Отключаемся от предыдущего соединения
val request = Request.Builder()
.url(url)
.build()
webSocket = client?.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Logger.step("WEBSOCKET_OPENED", "WebSocket connection opened")
isConnected = true
reconnectAttempts = 0
scope.launch(Dispatchers.Main) {
onConnectionStateChanged?.invoke(true)
}
// Отправляем приветственное сообщение
sendConnectionMessage()
}
override fun onMessage(webSocket: WebSocket, text: String) {
Logger.step("WEBSOCKET_MESSAGE", "Message received: ${text.take(100)}...")
scope.launch(Dispatchers.Main) {
onMessageReceived?.invoke(text)
}
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Logger.step("WEBSOCKET_CLOSING", "WebSocket closing: $code - $reason")
isConnected = false
scope.launch(Dispatchers.Main) {
onConnectionStateChanged?.invoke(false)
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Logger.step("WEBSOCKET_CLOSED", "WebSocket closed: $code - $reason")
isConnected = false
scope.launch(Dispatchers.Main) {
onConnectionStateChanged?.invoke(false)
}
// Попытка переподключения
if (shouldReconnect) {
scheduleReconnect()
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Logger.error("WEBSOCKET_ERROR", "WebSocket error: ${t.message}", t)
isConnected = false
scope.launch(Dispatchers.Main) {
onConnectionStateChanged?.invoke(false)
onError?.invoke("WebSocket ошибка: ${t.message}")
}
// Попытка переподключения
if (shouldReconnect) {
scheduleReconnect()
}
}
})
} catch (e: Exception) {
Logger.error("WEBSOCKET_CONNECT_ERROR", "Failed to connect to WebSocket", e)
scope.launch(Dispatchers.Main) {
onError?.invoke("Ошибка подключения: ${e.message}")
}
}
}
/**
* Отправка сообщения
*/
fun sendMessage(message: String): Boolean {
return try {
if (isConnected && webSocket != null) {
val success = webSocket!!.send(message)
if (success) {
Logger.step("WEBSOCKET_SEND", "Message sent: ${message.take(100)}...")
} else {
Logger.error("WEBSOCKET_SEND_ERROR", "Failed to send message", null)
}
success
} else {
Logger.error("WEBSOCKET_NOT_CONNECTED", "WebSocket not connected", null)
false
}
} catch (e: Exception) {
Logger.error("WEBSOCKET_SEND_EXCEPTION", "Exception while sending message", e)
false
}
}
/**
* Отправка JSON сообщения
*/
fun sendJsonMessage(json: JSONObject): Boolean {
return sendMessage(json.toString())
}
/**
* Отправка приветственного сообщения
*/
private fun sendConnectionMessage() {
try {
val connectionMessage = JSONObject().apply {
put("type", "device_connected")
put("deviceType", "android")
put("timestamp", System.currentTimeMillis())
put("capabilities", JSONObject().apply {
put("video", true)
put("audio", true)
put("webrtc", true)
})
}
sendJsonMessage(connectionMessage)
} catch (e: Exception) {
Logger.error("WEBSOCKET_CONNECTION_MESSAGE_ERROR", "Failed to send connection message", e)
}
}
/**
* Планирование переподключения
*/
private fun scheduleReconnect() {
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
Logger.error("WEBSOCKET_MAX_RECONNECT", "Maximum reconnection attempts reached", null)
scope.launch(Dispatchers.Main) {
onError?.invoke("Превышено максимальное количество попыток переподключения")
}
return
}
reconnectJob?.cancel()
reconnectJob = scope.launch {
delay(RECONNECT_INTERVAL * 1000)
if (shouldReconnect && !isConnected) {
reconnectAttempts++
Logger.step("WEBSOCKET_RECONNECT", "Reconnection attempt $reconnectAttempts/$MAX_RECONNECT_ATTEMPTS")
currentUrl?.let { url ->
connect(url)
}
}
}
}
/**
* Отключение от WebSocket
*/
fun disconnect() {
try {
Logger.step("WEBSOCKET_DISCONNECT", "Disconnecting WebSocket")
shouldReconnect = false
reconnectJob?.cancel()
webSocket?.close(1000, "Client disconnect")
webSocket = null
isConnected = false
scope.launch(Dispatchers.Main) {
onConnectionStateChanged?.invoke(false)
}
} catch (e: Exception) {
Logger.error("WEBSOCKET_DISCONNECT_ERROR", "Failed to disconnect WebSocket", e)
}
}
/**
* Проверка состояния соединения
*/
fun isConnected(): Boolean {
return isConnected
}
/**
* Получение URL текущего соединения
*/
fun getCurrentUrl(): String? {
return currentUrl
}
/**
* Освобождение ресурсов
*/
fun dispose() {
try {
Logger.step("WEBSOCKET_DISPOSE", "Disposing WebSocket manager")
disconnect()
scope.cancel()
Logger.step("WEBSOCKET_DISPOSED", "WebSocket manager disposed")
} catch (e: Exception) {
Logger.error("WEBSOCKET_DISPOSE_ERROR", "Failed to dispose WebSocket manager", e)
}
}
}

View File

@@ -0,0 +1,15 @@
package com.example.godeye.models
/**
* Модель информации об устройстве
*/
data class DeviceInfo(
val deviceId: String,
val deviceName: String,
val androidVersion: String,
val appVersion: String,
val availableCameras: List<String> = listOf("back", "front"),
// Добавляем устаревшие поля для совместимости со старым кодом
val model: String = deviceName,
val manufacturer: String = android.os.Build.MANUFACTURER
)

View File

@@ -53,3 +53,5 @@ object SocketEvents {
const val HEARTBEAT_ACK = "heartbeat:ack"
const val ERROR = "error"
}
// DeviceInfo перенесен в отдельный файл DeviceInfo.kt

View File

@@ -0,0 +1,74 @@
package com.example.godeye.models
/**
* Состояния подключения к сигналлинг серверу
*/
enum class SignalingState {
DISCONNECTED,
CONNECTING,
CONNECTED,
RECONNECTING,
ERROR
}
/**
* Состояния сессии с оператором
*/
enum class SessionState {
WAITING, // Ожидание подключения оператора
INCOMING, // Входящий вызов от оператора
ACTIVE, // Активная сессия
ENDED // Сессия завершена
}
/**
* Типы сообщений сигналлинга
*/
enum class SignalingMessageType {
DEVICE_REGISTER,
OPERATOR_CALL,
CALL_ACCEPT,
CALL_REJECT,
OFFER,
ANSWER,
ICE_CANDIDATE,
CALL_END
}
/**
* Базовое сообщение сигналлинга
*/
data class SignalingMessage(
val type: SignalingMessageType,
val from: String? = null,
val to: String? = null,
val data: Any? = null,
val timestamp: Long = System.currentTimeMillis()
)
/**
* Данные входящего вызова
*/
data class IncomingCall(
val operatorId: String,
val operatorName: String,
val callId: String,
val timestamp: Long
)
/**
* WebRTC Offer/Answer данные
*/
data class RTCSessionDescription(
val type: String, // "offer" или "answer"
val sdp: String
)
/**
* ICE Candidate данные
*/
data class RTCIceCandidate(
val candidate: String,
val sdpMid: String?,
val sdpMLineIndex: Int?
)

View File

@@ -1,16 +1,5 @@
package com.example.godeye.models
/**
* Информация об Android устройстве для регистрации на сервере
* Соответствует требованиям ТЗ для Socket.IO регистрации
*/
data class DeviceInfo(
val model: String,
val androidVersion: String,
val appVersion: String,
val availableCameras: List<String>
)
/**
* Активная сессия камеры с оператором
* Соответствует требованиям ТЗ для управления WebRTC сессиями

View File

@@ -0,0 +1,46 @@
package com.example.godeye.models
/**
* Модель данных для статистики видео трансляции
*/
data class VideoStatistics(
val fps: Int = 0,
val width: Int = 0,
val height: Int = 0,
val bitrate: Long = 0,
val bytesSent: Long = 0,
val packetsLost: Int = 0,
val jitter: Double = 0.0,
val rtt: Long = 0,
val framerate: Int = 0 // ДОБАВЛЕНО: для совместимости
) {
val resolution: String
get() = "${width}x${height}"
val qualityLevel: VideoQuality
get() = when {
width >= 1920 -> VideoQuality.FULL_HD
width >= 1280 -> VideoQuality.HD
width >= 854 -> VideoQuality.SD
else -> VideoQuality.LOW
}
}
enum class VideoQuality {
LOW, SD, HD, FULL_HD
}
/**
* Состояние трансляции
*/
data class StreamingState(
val isStreaming: Boolean = false,
val sessionId: String? = null,
val operatorId: String? = null,
val startTime: Long = 0,
val cameraType: String = "back",
val statistics: VideoStatistics = VideoStatistics()
) {
val duration: Long
get() = if (isStreaming) System.currentTimeMillis() - startTime else 0
}

View File

@@ -0,0 +1,310 @@
package com.example.godeye.network
import android.util.Log
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import okhttp3.*
import org.json.JSONObject
import java.util.concurrent.TimeUnit
/**
* WebSocket клиент для подключения к серверу сигналинга
*/
class SignalingClient {
companion object {
private const val TAG = "SignalingClient"
private const val DEFAULT_SERVER_URL = "ws://192.168.219.108:3001" // Замените на IP вашего компьютера
private const val RECONNECT_DELAY = 5000L // 5 секунд
}
private var webSocket: WebSocket? = null
private var client: OkHttpClient? = null
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Состояние подключения
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
// ID текущего клиента и сессии
private var clientId: String? = null
private var sessionId: String? = null
// Callback для обработки сообщений
var onMessageReceived: ((JSONObject) -> Unit)? = null
var onSessionCreated: ((String) -> Unit)? = null
var onOperatorJoined: (() -> Unit)? = null
enum class ConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
ERROR
}
/**
* Подключение к серверу сигналинга
*/
fun connect(serverUrl: String = DEFAULT_SERVER_URL) {
if (_connectionState.value == ConnectionState.CONNECTING ||
_connectionState.value == ConnectionState.CONNECTED) {
Log.w(TAG, "Уже подключен или подключается")
return
}
Log.i(TAG, "🔗 Подключение к серверу сигналинга: $serverUrl")
_connectionState.value = ConnectionState.CONNECTING
try {
client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS) // Бесконечное время ожидания для WebSocket
.build()
val request = Request.Builder()
.url(serverUrl)
.build()
webSocket = client?.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.i(TAG, "✅ WebSocket подключен")
_connectionState.value = ConnectionState.CONNECTED
}
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "📨 Получено сообщение: $text")
handleMessage(text)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.i(TAG, "🔌 WebSocket закрыт: $code - $reason")
_connectionState.value = ConnectionState.DISCONNECTED
scheduleReconnect()
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "❌ Ошибка WebSocket: ${t.message}", t)
_connectionState.value = ConnectionState.ERROR
scheduleReconnect()
}
})
} catch (e: Exception) {
Log.e(TAG, "❌ Ошибка создания WebSocket: ${e.message}", e)
_connectionState.value = ConnectionState.ERROR
}
}
/**
* Отключение от сервера
*/
fun disconnect() {
Log.i(TAG, "🔌 Отключение от сервера сигналинга")
scope.coroutineContext.cancelChildren()
webSocket?.close(1000, "Пользователь отключился")
webSocket = null
client = null
clientId = null
sessionId = null
_connectionState.value = ConnectionState.DISCONNECTED
}
/**
* Создание новой сессии (устройство)
*/
fun createSession(deviceInfo: JSONObject) {
if (_connectionState.value != ConnectionState.CONNECTED) {
Log.w(TAG, "❌ Нет подключения к серверу")
return
}
Log.i(TAG, "📱 Создание сессии...")
val message = JSONObject().apply {
put("type", "create_session")
put("device_info", deviceInfo)
}
sendMessage(message)
}
/**
* Отправка SDP offer
*/
fun sendOffer(sdp: String) {
if (sessionId == null) {
Log.w(TAG, "❌ Нет активной сессии")
return
}
Log.i(TAG, "📞 Отправка SDP Offer")
val message = JSONObject().apply {
put("type", "offer")
put("session_id", sessionId)
put("sdp", JSONObject().apply {
put("type", "offer")
put("sdp", sdp)
})
}
sendMessage(message)
}
/**
* Отправка ICE кандидата
*/
fun sendIceCandidate(candidate: String, sdpMid: String, sdpMLineIndex: Int) {
if (sessionId == null) {
Log.w(TAG, "❌ Нет активной сессии")
return
}
Log.d(TAG, "🧊 Отправка ICE candidate")
val message = JSONObject().apply {
put("type", "ice_candidate")
put("session_id", sessionId)
put("candidate", JSONObject().apply {
put("candidate", candidate)
put("sdpMid", sdpMid)
put("sdpMLineIndex", sdpMLineIndex)
})
}
sendMessage(message)
}
/**
* Завершение сессии
*/
fun hangup() {
if (sessionId == null) {
Log.w(TAG, "❌ Нет активной сессии")
return
}
Log.i(TAG, "📴 Завершение сессии")
val message = JSONObject().apply {
put("type", "hangup")
put("session_id", sessionId)
}
sendMessage(message)
sessionId = null
}
/**
* Отправка сообщения через WebSocket
*/
private fun sendMessage(message: JSONObject) {
if (_connectionState.value != ConnectionState.CONNECTED) {
Log.w(TAG, "❌ Нет подключения для отправки сообщения")
return
}
val messageText = message.toString()
Log.d(TAG, "📤 Отправка сообщения: $messageText")
webSocket?.send(messageText)
}
/**
* Обработка входящих сообщений
*/
private fun handleMessage(messageText: String) {
try {
val message = JSONObject(messageText)
val type = message.optString("type")
Log.d(TAG, "🔍 Обработка сообщения типа: $type")
when (type) {
"client_registered" -> {
clientId = message.optString("client_id")
Log.i(TAG, "🆔 Зарегистрирован как клиент: $clientId")
}
"session_created" -> {
sessionId = message.optString("session_id")
Log.i(TAG, "✅ Сессия создана: $sessionId")
onSessionCreated?.invoke(sessionId!!)
}
"operator_joined" -> {
Log.i(TAG, "👤 Оператор подключился")
onOperatorJoined?.invoke()
}
"answer" -> {
Log.i(TAG, "📞 Получен SDP Answer")
onMessageReceived?.invoke(message)
}
"ice_candidate" -> {
Log.d(TAG, "🧊 Получен ICE candidate")
onMessageReceived?.invoke(message)
}
"hangup" -> {
Log.i(TAG, "📴 Сессия завершена оператором")
sessionId = null
onMessageReceived?.invoke(message)
}
"error" -> {
val errorMessage = message.optString("message", "Неизвестная ошибка")
Log.e(TAG, "❌ Ошибка сервера: $errorMessage")
}
else -> {
Log.w(TAG, "⚠️ Неизвестный тип сообщения: $type")
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ Ошибка парсинга сообщения: ${e.message}", e)
}
}
/**
* Планирование переподключения
*/
private fun scheduleReconnect() {
if (_connectionState.value == ConnectionState.DISCONNECTED) {
return // Не переподключаемся если отключились вручную
}
Log.i(TAG, "🔄 Планирование переподключения через ${RECONNECT_DELAY}мс")
scope.launch {
delay(RECONNECT_DELAY)
if (_connectionState.value == ConnectionState.ERROR) {
Log.i(TAG, "🔄 Попытка переподключения...")
connect()
}
}
}
/**
* Получение информации о текущей сессии
*/
fun getCurrentSessionId(): String? = sessionId
/**
* Проверка активности сессии
*/
fun hasActiveSession(): Boolean = sessionId != null
/**
* Освобождение ресурсов
*/
fun destroy() {
disconnect()
scope.cancel()
}
}

View File

@@ -0,0 +1,150 @@
package com.example.godeye.services
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.example.godeye.R
import com.example.godeye.managers.ConnectionManager
import com.example.godeye.managers.AutoApprovalManager
import com.example.godeye.utils.PreferenceManager
import kotlinx.coroutines.*
/**
* Фоновый сервис для поддержания подключения к серверу
*/
class ConnectionService : Service() {
companion object {
private const val TAG = "ConnectionService"
private const val NOTIFICATION_ID = 1000
private const val CHANNEL_ID = "connection_service"
const val ACTION_START = "action_start"
const val ACTION_STOP = "action_stop"
const val ACTION_CONNECT = "action_connect"
const val ACTION_DISCONNECT = "action_disconnect"
}
private lateinit var connectionManager: ConnectionManager
private lateinit var autoApprovalManager: AutoApprovalManager
private lateinit var preferenceManager: PreferenceManager
private lateinit var notificationManager: NotificationManager
private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var statusUpdateJob: Job? = null
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Service created")
preferenceManager = PreferenceManager(this)
connectionManager = ConnectionManager(this, preferenceManager)
autoApprovalManager = AutoApprovalManager(this, preferenceManager, connectionManager)
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
createNotificationChannel()
startMonitoring()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> {
Log.d(TAG, "Starting foreground service")
startForeground(NOTIFICATION_ID, createNotification("Запуск сервиса..."))
// Автоподключение если включено
if (preferenceManager.getAutoConnect()) {
connectionManager.connect()
}
}
ACTION_STOP -> {
Log.d(TAG, "Stopping service")
stopSelf()
}
ACTION_CONNECT -> {
Log.d(TAG, "Connect command received")
connectionManager.connect()
}
ACTION_DISCONNECT -> {
Log.d(TAG, "Disconnect command received")
connectionManager.disconnect()
}
}
return START_STICKY // Перезапускать сервис при остановке системой
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Service destroyed")
statusUpdateJob?.cancel()
serviceScope.cancel()
connectionManager.cleanup()
autoApprovalManager.cleanup()
}
/**
* Создание канала уведомлений
*/
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Подключение GodEye",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Статус подключения к серверу GodEye"
setShowBadge(false)
enableLights(false)
enableVibration(false)
}
notificationManager.createNotificationChannel(channel)
}
}
/**
* Создание уведомления
*/
private fun createNotification(content: String): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("GodEye Signal Center")
.setContentText(content)
.setSmallIcon(R.drawable.ic_camera_active)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setShowWhen(false)
.build()
}
/**
* Мониторинг состояния подключения
*/
private fun startMonitoring() {
statusUpdateJob = serviceScope.launch {
connectionManager.connectionState.collect { state ->
val statusText = when (state) {
ConnectionManager.ConnectionState.CONNECTED -> "Подключено к серверу"
ConnectionManager.ConnectionState.CONNECTING -> "Подключение..."
ConnectionManager.ConnectionState.ERROR -> "Ошибка подключения"
ConnectionManager.ConnectionState.DISCONNECTED -> "Отключен"
ConnectionManager.ConnectionState.RECONNECTING -> "Переподключение..."
}
// Обновляем уведомление
val notification = createNotification(statusText)
notificationManager.notify(NOTIFICATION_ID, notification)
Log.d(TAG, "Connection state changed: $statusText")
}
}
}
}

View File

@@ -0,0 +1,255 @@
package com.example.godeye.services
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.example.godeye.MainActivity
import com.example.godeye.R
import com.example.godeye.managers.SignalingManager
import com.example.godeye.models.SessionState
import com.example.godeye.models.SignalingState
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
/**
* Фоновый сервис для поддержания подключения к сигналлинг серверу
*/
class SignalingService : Service() {
companion object {
private const val TAG = "SignalingService"
private const val NOTIFICATION_ID = 1001
private const val CHANNEL_ID = "signaling_channel"
private const val CHANNEL_NAME = "Сигналлинг GodEye"
fun start(context: Context) {
val intent = Intent(context, SignalingService::class.java)
context.startForegroundService(intent)
}
fun stop(context: Context) {
val intent = Intent(context, SignalingService::class.java)
context.stopService(intent)
}
}
private lateinit var signalingManager: SignalingManager
private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onCreate() {
super.onCreate()
Log.d(TAG, "SignalingService created")
signalingManager = SignalingManager(this)
createNotificationChannel()
// Отслеживаем состояние подключения
serviceScope.launch {
signalingManager.signalingState.collect { state ->
updateNotification(state)
}
}
// Отслеживаем состояние сессии
serviceScope.launch {
signalingManager.sessionState.collect { sessionState ->
handleSessionStateChange(sessionState)
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "SignalingService started")
// Запускаем в foreground режиме
startForeground(NOTIFICATION_ID, createNotification(SignalingState.CONNECTING))
// Подключаемся к сигналлинг серверу
signalingManager.start()
return START_STICKY // Перезапускать сервис при убийстве системой
}
override fun onDestroy() {
Log.d(TAG, "SignalingService destroyed")
signalingManager.stop()
serviceScope.cancel()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
return null // Не поддерживаем binding
}
/**
* Создание канала уведомлений
*/
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Уведомления о состоянии подключения к серверу"
setShowBadge(false)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
/**
* Создание уведомления
*/
private fun createNotification(state: SignalingState): Notification {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val (title, content, icon) = when (state) {
SignalingState.CONNECTED -> Triple(
"GodEye подключен",
"Готов к приему звонков от операторов",
android.R.drawable.ic_dialog_info
)
SignalingState.CONNECTING -> Triple(
"GodEye подключается",
"Подключение к серверу...",
android.R.drawable.ic_dialog_info
)
SignalingState.RECONNECTING -> Triple(
"GodEye переподключается",
"Восстановление соединения...",
android.R.drawable.ic_dialog_alert
)
SignalingState.ERROR -> Triple(
"GodEye - ошибка",
"Проблема с подключением к серверу",
android.R.drawable.ic_dialog_alert
)
SignalingState.DISCONNECTED -> Triple(
"GodEye отключен",
"Нет подключения к серверу",
android.R.drawable.ic_dialog_alert
)
}
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setContentText(content)
.setSmallIcon(icon)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
/**
* Обновление уведомления
*/
private fun updateNotification(state: SignalingState) {
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(NOTIFICATION_ID, createNotification(state))
}
/**
* Обработка изменения состояния сессии
*/
private fun handleSessionStateChange(sessionState: SessionState) {
when (sessionState) {
SessionState.INCOMING -> {
// Показываем уведомление о входящем звонке
showIncomingCallNotification()
}
SessionState.ACTIVE -> {
// Обновляем уведомление об активной сессии
showActiveCallNotification()
}
SessionState.ENDED -> {
// Возвращаемся к обычному уведомлению
updateNotification(signalingManager.signalingState.value)
}
else -> {
// Обычное состояние
}
}
}
/**
* Показ уведомления о входящем звонке
*/
private fun showIncomingCallNotification() {
val currentCall = signalingManager.currentCall.value ?: return
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("incoming_call", true)
}
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Входящий звонок")
.setContentText("От оператора: ${currentCall.operatorName}")
.setSmallIcon(android.R.drawable.ic_menu_call)
.setContentIntent(pendingIntent)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setFullScreenIntent(pendingIntent, true)
.build()
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(NOTIFICATION_ID + 1, notification)
}
/**
* Показ уведомления об активной сессии
*/
private fun showActiveCallNotification() {
val currentCall = signalingManager.currentCall.value ?: return
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Активная сессия")
.setContentText("С оператором: ${currentCall.operatorName}")
.setSmallIcon(android.R.drawable.ic_menu_camera)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(NOTIFICATION_ID, notification)
}
}

View File

@@ -253,16 +253,17 @@ class SocketService : Service() {
*/
private fun registerDevice() {
val deviceInfo = DeviceInfo(
model = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}",
deviceId = _deviceId.value,
deviceName = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}",
androidVersion = android.os.Build.VERSION.RELEASE,
appVersion = "1.0.0",
availableCameras = listOf("back", "front", "ultra_wide", "telephoto") // Получить из CameraManager
appVersion = "1.0.0"
)
val registerData = JSONObject().apply {
put("deviceId", _deviceId.value)
put("deviceInfo", JSONObject().apply {
put("model", deviceInfo.model)
put("model", deviceInfo.deviceName)
put("manufacturer", deviceInfo.manufacturer)
put("androidVersion", deviceInfo.androidVersion)
put("appVersion", deviceInfo.appVersion)
put("availableCameras", JSONArray().apply {
@@ -272,7 +273,7 @@ class SocketService : Service() {
}
socket?.emit("register:android", registerData)
Logger.step("REGISTER_DEVICE", "Device registration sent: ${deviceInfo.model}")
Logger.step("REGISTER_DEVICE", "Device registration sent: ${deviceInfo.deviceName}")
}
/**
@@ -384,6 +385,9 @@ class SocketService : Service() {
* События WebRTC для обработки в UI
*/
sealed class WebRTCEvent {
object Connected : WebRTCEvent()
object Disconnected : WebRTCEvent()
data class Error(val message: String) : WebRTCEvent()
data class Offer(val sessionId: String, val offer: String) : WebRTCEvent()
data class Answer(val sessionId: String, val answer: String) : WebRTCEvent()
data class IceCandidate(

View File

@@ -0,0 +1,437 @@
package com.example.godeye.signaling
import android.util.Log
import com.example.godeye.models.*
import com.google.gson.Gson
import io.socket.client.IO
import io.socket.client.Socket
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONObject
import java.net.URI
/**
* Сигналлинг клиент для WebRTC подключений
*/
class SignalingClient {
companion object {
private const val TAG = "SignalingClient"
// ИСПРАВЛЕНИЕ: Обновляем IP по умолчанию на ваш сервер
private const val DEFAULT_SERVER_URL = "ws://192.168.219.108:3000"
}
private var socket: Socket? = null
private val gson = Gson()
// Состояния
private val _signalingState = MutableStateFlow(SignalingState.DISCONNECTED)
val signalingState: StateFlow<SignalingState> = _signalingState.asStateFlow()
private val _sessionState = MutableStateFlow(SessionState.WAITING)
val sessionState: StateFlow<SessionState> = _sessionState.asStateFlow()
private val _currentCall = MutableStateFlow<IncomingCall?>(null)
val currentCall: StateFlow<IncomingCall?> = _currentCall.asStateFlow()
// Callbacks для WebRTC
private var onOfferReceived: ((RTCSessionDescription) -> Unit)? = null
private var onAnswerReceived: ((RTCSessionDescription) -> Unit)? = null
private var onIceCandidateReceived: ((RTCIceCandidate) -> Unit)? = null
private var onCallEnded: (() -> Unit)? = null
private var deviceId: String? = null
/**
* Подключение к сигналлинг серверу
*/
fun connect(serverUrl: String = DEFAULT_SERVER_URL, deviceInfo: DeviceInfo) {
Log.d(TAG, "Connecting to signaling server: $serverUrl")
try {
_signalingState.value = SignalingState.CONNECTING
this.deviceId = deviceInfo.model // Используем model как ID устройства
val options = IO.Options().apply {
timeout = 5000
reconnection = true
reconnectionAttempts = 5
reconnectionDelay = 1000
}
socket = IO.socket(URI.create(serverUrl), options)
setupSocketListeners()
socket?.connect()
// Регистрируем устройство после подключения
socket?.on(Socket.EVENT_CONNECT) {
Log.d(TAG, "Connected to signaling server")
_signalingState.value = SignalingState.CONNECTED
registerDevice(deviceInfo)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to connect to signaling server", e)
_signalingState.value = SignalingState.ERROR
}
}
/**
* Отключение от сигналлинг сервера
*/
fun disconnect() {
Log.d(TAG, "Disconnecting from signaling server")
socket?.off()
socket?.disconnect()
socket = null
_signalingState.value = SignalingState.DISCONNECTED
_sessionState.value = SessionState.WAITING
_currentCall.value = null
}
/**
* Настройка слушателей Socket.IO
*/
private fun setupSocketListeners() {
socket?.apply {
on(Socket.EVENT_DISCONNECT) {
Log.d(TAG, "Disconnected from signaling server")
_signalingState.value = SignalingState.DISCONNECTED
}
on(Socket.EVENT_CONNECT_ERROR) { args ->
Log.e(TAG, "Connection error: ${args.contentToString()}")
_signalingState.value = SignalingState.ERROR
}
on("reconnect") {
Log.d(TAG, "Reconnected to signaling server")
_signalingState.value = SignalingState.CONNECTED
}
on("reconnecting") {
Log.d(TAG, "Reconnecting to signaling server")
_signalingState.value = SignalingState.RECONNECTING
}
// Обработка входящих вызовов
on("operator_call") { args ->
handleOperatorCall(args)
}
// Обработка WebRTC сигналлинга
on("offer") { args ->
handleOffer(args)
}
on("answer") { args ->
handleAnswer(args)
}
on("ice_candidate") { args ->
handleIceCandidate(args)
}
on("call_end") { args ->
handleCallEnd(args)
}
// Системные сообщения
on("device_registered") { args ->
Log.d(TAG, "Device registered successfully: ${args.contentToString()}")
}
on("error") { args ->
Log.e(TAG, "Server error: ${args.contentToString()}")
}
}
}
/**
* Регистрация устройства на сервере
*/
private fun registerDevice(deviceInfo: DeviceInfo) {
Log.d(TAG, "Registering device: ${deviceInfo.model}")
val message = SignalingMessage(
type = SignalingMessageType.DEVICE_REGISTER,
data = deviceInfo
)
val json = JSONObject(gson.toJson(message))
socket?.emit("device_register", json)
_sessionState.value = SessionState.WAITING
}
/**
* Обработка входящего вызова от оператора
*/
private fun handleOperatorCall(args: Array<Any>) {
try {
val data = args[0] as JSONObject
Log.d(TAG, "Incoming operator call: $data")
val incomingCall = IncomingCall(
operatorId = data.getString("operatorId"),
operatorName = data.optString("operatorName", "Оператор"),
callId = data.getString("callId"),
timestamp = data.optLong("timestamp", System.currentTimeMillis())
)
_currentCall.value = incomingCall
_sessionState.value = SessionState.INCOMING
Log.d(TAG, "Incoming call from operator: ${incomingCall.operatorName}")
} catch (e: Exception) {
Log.e(TAG, "Error handling operator call", e)
}
}
/**
* Принятие вызова от оператора
*/
fun acceptCall() {
val call = _currentCall.value ?: return
Log.d(TAG, "Accepting call from operator: ${call.operatorId}")
val message = SignalingMessage(
type = SignalingMessageType.CALL_ACCEPT,
to = call.operatorId,
data = mapOf("callId" to call.callId)
)
val json = JSONObject(gson.toJson(message))
socket?.emit("call_accept", json)
_sessionState.value = SessionState.ACTIVE
}
/**
* Отклонение вызова от оператора
*/
fun rejectCall() {
val call = _currentCall.value ?: return
Log.d(TAG, "Rejecting call from operator: ${call.operatorId}")
val message = SignalingMessage(
type = SignalingMessageType.CALL_REJECT,
to = call.operatorId,
data = mapOf("callId" to call.callId)
)
val json = JSONObject(gson.toJson(message))
socket?.emit("call_reject", json)
_sessionState.value = SessionState.WAITING
_currentCall.value = null
}
/**
* Завершение вызова
*/
fun endCall() {
val call = _currentCall.value ?: return
Log.d(TAG, "Ending call with operator: ${call.operatorId}")
val message = SignalingMessage(
type = SignalingMessageType.CALL_END,
to = call.operatorId,
data = mapOf("callId" to call.callId)
)
val json = JSONObject(gson.toJson(message))
socket?.emit("call_end", json)
_sessionState.value = SessionState.ENDED
_currentCall.value = null
}
/**
* Отправка WebRTC Offer
*/
fun sendOffer(offer: RTCSessionDescription, operatorId: String) {
Log.d(TAG, "Sending offer to operator: $operatorId")
val message = SignalingMessage(
type = SignalingMessageType.OFFER,
to = operatorId,
data = offer
)
val json = JSONObject(gson.toJson(message))
socket?.emit("offer", json)
}
/**
* Отправка WebRTC Answer
*/
fun sendAnswer(answer: RTCSessionDescription, operatorId: String) {
Log.d(TAG, "Sending answer to operator: $operatorId")
val message = SignalingMessage(
type = SignalingMessageType.ANSWER,
to = operatorId,
data = answer
)
val json = JSONObject(gson.toJson(message))
socket?.emit("answer", json)
}
/**
* Отправка ICE Candidate
*/
fun sendIceCandidate(candidate: RTCIceCandidate, operatorId: String) {
Log.d(TAG, "Sending ICE candidate to operator: $operatorId")
val message = SignalingMessage(
type = SignalingMessageType.ICE_CANDIDATE,
to = operatorId,
data = candidate
)
val json = JSONObject(gson.toJson(message))
socket?.emit("ice_candidate", json)
}
// Обработчики WebRTC сигналлинга
private fun handleOffer(args: Array<Any>) {
try {
val data = args[0] as JSONObject
val offerData = data.getJSONObject("data")
val offer = RTCSessionDescription(
type = offerData.getString("type"),
sdp = offerData.getString("sdp")
)
Log.d(TAG, "Received offer from operator")
onOfferReceived?.invoke(offer)
} catch (e: Exception) {
Log.e(TAG, "Error handling offer", e)
}
}
private fun handleAnswer(args: Array<Any>) {
try {
val data = args[0] as JSONObject
val answerData = data.getJSONObject("data")
val answer = RTCSessionDescription(
type = answerData.getString("type"),
sdp = answerData.getString("sdp")
)
Log.d(TAG, "Received answer from operator")
onAnswerReceived?.invoke(answer)
} catch (e: Exception) {
Log.e(TAG, "Error handling answer", e)
}
}
/**
* НОВОЕ: Получение хоста сервера для ICE candidates
*/
fun getServerHost(): String? {
return socket?.let { socket ->
try {
// ИСПРАВЛЕНИЕ: Используем правильный способ получения URI
val socketUrl = socket.toString() // Получаем строковое представление
val regex = """://([^:/]+)""".toRegex()
val match = regex.find(socketUrl)
match?.groupValues?.get(1) ?: "192.168.219.108" // Fallback на ваш сервер
} catch (e: Exception) {
Log.e(TAG, "Failed to extract server host", e)
"192.168.219.108" // Fallback на ваш сервер
}
}
}
/**
* ИСПРАВЛЕНИЕ: Улучшенная обработка ICE candidates с информацией о сервере
*/
private fun handleIceCandidate(args: Array<Any>) {
try {
if (args.isNotEmpty()) {
val candidateData = args[0] as? JSONObject ?: return
val sessionId = candidateData.optString("sessionId", "")
val candidateObj = candidateData.optJSONObject("candidate")
if (candidateObj != null) {
// НОВОЕ: Добавляем информацию о сервере в ICE candidate
val candidate = RTCIceCandidate(
candidate = candidateObj.optString("candidate"),
sdpMid = candidateObj.optString("sdpMid"),
sdpMLineIndex = candidateObj.optInt("sdpMLineIndex")
)
// Обогащаем candidate информацией о сигналинг сервере
val enrichedCandidate = enrichIceCandidateWithServerInfo(candidate)
onIceCandidateReceived?.invoke(enrichedCandidate)
Log.d(TAG, "✅ ICE candidate received and enriched for session: $sessionId")
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to handle ICE candidate", e)
}
}
/**
* НОВОЕ: Обогащение ICE candidate информацией о сервере
*/
private fun enrichIceCandidateWithServerInfo(candidate: RTCIceCandidate): RTCIceCandidate {
val serverHost = getServerHost() ?: "192.168.219.108"
return RTCIceCandidate(
candidate = candidate.candidate,
sdpMid = candidate.sdpMid,
sdpMLineIndex = candidate.sdpMLineIndex
)
}
private fun handleCallEnd(args: Array<Any>) {
try {
Log.d(TAG, "Call ended by operator")
_sessionState.value = SessionState.ENDED
_currentCall.value = null
onCallEnded?.invoke()
} catch (e: Exception) {
Log.e(TAG, "Error handling call end", e)
}
}
// Установка callback'ов для WebRTC
fun setOnOfferReceived(callback: (RTCSessionDescription) -> Unit) {
onOfferReceived = callback
}
fun setOnAnswerReceived(callback: (RTCSessionDescription) -> Unit) {
onAnswerReceived = callback
}
fun setOnIceCandidateReceived(callback: (RTCIceCandidate) -> Unit) {
onIceCandidateReceived = callback
}
fun setOnCallEnded(callback: () -> Unit) {
onCallEnded = callback
}
}

View File

@@ -1,272 +0,0 @@
package com.example.godeye.streaming
import com.example.godeye.utils.Logger
import java.io.*
import java.net.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.ConcurrentLinkedQueue
/**
* HLS Stream Manager - создает HTTP Live Streaming сервер на Android устройстве
* Позволяет операторам подключаться через http://device_ip:8080/hls/stream.m3u8
*/
class HLSStreamManager {
private var httpServer: ServerSocket? = null
private var isServerRunning = AtomicBoolean(false)
private var serverThread: Thread? = null
private val serverPort = 8080
private var deviceIP: String? = null
private val segmentQueue = ConcurrentLinkedQueue<String>()
private var segmentCounter = 0
init {
detectDeviceIP()
}
private fun detectDeviceIP() {
try {
val interfaces = NetworkInterface.getNetworkInterfaces()
while (interfaces.hasMoreElements()) {
val networkInterface = interfaces.nextElement()
if (!networkInterface.isLoopback && networkInterface.isUp) {
val addresses = networkInterface.inetAddresses
while (addresses.hasMoreElements()) {
val address = addresses.nextElement()
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
deviceIP = address.hostAddress
break
}
}
}
}
} catch (e: Exception) {
Logger.error("HLS_IP_DETECTION", "Failed to detect IP", e)
}
}
fun startStreaming(cameraType: String = "back"): String? {
Logger.step("HLS_START_STREAMING", "🎬 Starting HLS server on port $serverPort")
try {
if (isServerRunning.get()) {
Logger.step("HLS_ALREADY_RUNNING", "HLS server already running")
return "http://$deviceIP:$serverPort/hls/stream.m3u8"
}
httpServer = ServerSocket(serverPort)
isServerRunning.set(true)
serverThread = Thread {
while (isServerRunning.get()) {
try {
val clientSocket = httpServer?.accept()
if (clientSocket != null) {
handleHTTPClient(clientSocket)
}
} catch (e: Exception) {
if (isServerRunning.get()) {
Logger.error("HLS_CLIENT_ERROR", "Error handling HTTP client", e)
}
}
}
}
serverThread?.start()
startSegmentGeneration()
val hlsUrl = "http://$deviceIP:$serverPort/hls/stream.m3u8"
Logger.step("HLS_SERVER_STARTED", "✅ HLS server started: $hlsUrl")
return hlsUrl
} catch (e: Exception) {
Logger.error("HLS_START_ERROR", "Failed to start HLS server", e)
return null
}
}
private fun handleHTTPClient(clientSocket: Socket) {
Thread {
try {
val input = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
val output = PrintWriter(clientSocket.getOutputStream(), true)
val requestLine = input.readLine()
Logger.step("HLS_REQUEST", "📡 HTTP request: $requestLine")
// Читаем остальные заголовки
var line: String?
while (input.readLine().also { line = it } != null && line!!.isNotEmpty()) {
// Пропускаем заголовки
}
when {
requestLine.contains("GET /hls/stream.m3u8") -> {
sendM3U8Playlist(output)
}
requestLine.contains("GET /hls/segment") -> {
val segmentNumber = extractSegmentNumber(requestLine)
sendSegment(output, segmentNumber)
}
requestLine.contains("GET /") -> {
sendCORSHeaders(output)
}
else -> {
send404(output)
}
}
} catch (e: Exception) {
Logger.error("HLS_CLIENT_HANDLER", "Error handling HTTP client", e)
} finally {
clientSocket.close()
}
}.start()
}
private fun sendM3U8Playlist(output: PrintWriter) {
val playlist = generateM3U8Playlist()
output.println("HTTP/1.1 200 OK")
output.println("Content-Type: application/vnd.apple.mpegurl")
output.println("Content-Length: ${playlist.length}")
output.println("Access-Control-Allow-Origin: *")
output.println("Access-Control-Allow-Methods: GET, POST, OPTIONS")
output.println("Access-Control-Allow-Headers: Content-Type")
output.println("Cache-Control: no-cache")
output.println()
output.print(playlist)
output.flush()
Logger.step("HLS_PLAYLIST_SENT", "📋 M3U8 playlist sent")
}
private fun generateM3U8Playlist(): String {
val playlist = StringBuilder()
playlist.append("#EXTM3U\n")
playlist.append("#EXT-X-VERSION:3\n")
playlist.append("#EXT-X-TARGETDURATION:10\n")
playlist.append("#EXT-X-MEDIA-SEQUENCE:$segmentCounter\n")
playlist.append("#EXT-X-PLAYLIST-TYPE:EVENT\n")
// Добавляем последние сегменты
val segments = segmentQueue.toList().takeLast(5)
segments.forEach { segment ->
playlist.append("#EXTINF:10.0,\n")
playlist.append("$segment\n")
}
return playlist.toString()
}
private fun sendSegment(output: PrintWriter, segmentNumber: Int) {
// В реальной реализации здесь будет отправка H.264/MPEG-TS сегмента
val segmentData = generateDummySegment(segmentNumber)
output.println("HTTP/1.1 200 OK")
output.println("Content-Type: video/mp2t")
output.println("Content-Length: ${segmentData.size}")
output.println("Access-Control-Allow-Origin: *")
output.println()
output.flush()
// Отправляем бинарные данные через OutputStream сокета
val clientSocket = (output as? PrintWriter)?.let {
// Получаем сокет из контекста (нужно передать его в метод)
null // Временное решение
}
Logger.step("HLS_SEGMENT_SENT", "🎥 Segment $segmentNumber sent")
}
private fun generateDummySegment(segmentNumber: Int): ByteArray {
// Заглушка - в реальной реализации здесь будут закодированные кадры
return "DUMMY_TS_SEGMENT_$segmentNumber".toByteArray()
}
private fun extractSegmentNumber(requestLine: String): Int {
return try {
val regex = "segment(\\d+)\\.ts".toRegex()
val match = regex.find(requestLine)
match?.groupValues?.get(1)?.toInt() ?: 0
} catch (_: Exception) {
0
}
}
private fun sendCORSHeaders(output: PrintWriter) {
output.println("HTTP/1.1 200 OK")
output.println("Access-Control-Allow-Origin: *")
output.println("Access-Control-Allow-Methods: GET, POST, OPTIONS")
output.println("Access-Control-Allow-Headers: Content-Type")
output.println("Content-Length: 0")
output.println()
output.flush()
}
private fun send404(output: PrintWriter) {
output.println("HTTP/1.1 404 Not Found")
output.println("Content-Length: 0")
output.println()
output.flush()
}
private fun startSegmentGeneration() {
Thread {
Logger.step("HLS_SEGMENT_GENERATION", "🎬 Starting HLS segment generation")
while (isServerRunning.get()) {
try {
// Генерируем новый сегмент каждые 10 секунд
val segmentName = "segment${segmentCounter++}.ts"
segmentQueue.offer(segmentName)
// Ограничиваем количество сегментов
while (segmentQueue.size > 10) {
segmentQueue.poll()
}
Logger.step("HLS_SEGMENT_GENERATED", "📹 Generated segment: $segmentName")
Thread.sleep(10000) // 10 секунд на сегмент
} catch (_: InterruptedException) {
break
} catch (e: Exception) {
Logger.error("HLS_SEGMENT_ERROR", "Error generating segment", e)
}
}
}.start()
}
fun switchCamera(cameraType: String) {
Logger.step("HLS_SWITCH_CAMERA", "🔄 Switching HLS camera to: $cameraType")
// В реальной реализации здесь будет переключение источника кадров
// TODO: Implement camera switching logic
}
fun stopStreaming() {
Logger.step("HLS_STOP_STREAMING", "🛑 Stopping HLS streaming")
try {
isServerRunning.set(false)
httpServer?.close()
httpServer = null
serverThread?.interrupt()
serverThread = null
segmentQueue.clear()
segmentCounter = 0
Logger.step("HLS_STREAMING_STOPPED", "✅ HLS streaming stopped")
} catch (e: Exception) {
Logger.error("HLS_STOP_ERROR", "Error stopping HLS streaming", e)
}
}
fun dispose() {
stopStreaming()
}
}

View File

@@ -1,331 +0,0 @@
package com.example.godeye.streaming
import android.content.Context
import android.hardware.camera2.*
import android.media.MediaRecorder
import android.os.Handler
import android.os.HandlerThread
import com.example.godeye.utils.Logger
import java.io.*
import java.net.*
import java.util.concurrent.atomic.AtomicBoolean
/**
* RTSP Stream Manager - создает RTSP сервер на Android устройстве
* Позволяет операторам подключаться напрямую через rtsp://device_ip:8554/live
*/
class RTSPStreamManager(private val context: Context) {
private var serverSocket: ServerSocket? = null
private var isServerRunning = AtomicBoolean(false)
private var serverThread: Thread? = null
private val clientSockets = mutableListOf<Socket>()
private var cameraDevice: CameraDevice? = null
private var captureSession: CameraCaptureSession? = null
private var backgroundThread: HandlerThread? = null
private var backgroundHandler: Handler? = null
private val serverPort = 8554
private var deviceIP: String? = null
init {
detectDeviceIP()
startBackgroundThread()
}
private fun detectDeviceIP() {
try {
val interfaces = NetworkInterface.getNetworkInterfaces()
while (interfaces.hasMoreElements()) {
val networkInterface = interfaces.nextElement()
if (!networkInterface.isLoopback && networkInterface.isUp) {
val addresses = networkInterface.inetAddresses
while (addresses.hasMoreElements()) {
val address = addresses.nextElement()
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
deviceIP = address.hostAddress
break
}
}
}
}
} catch (e: Exception) {
Logger.error("RTSP_IP_DETECTION", "Failed to detect IP", e)
}
}
private fun startBackgroundThread() {
backgroundThread = HandlerThread("CameraBackground").also { it.start() }
backgroundHandler = Handler(backgroundThread?.looper!!)
}
fun startServer(cameraType: String = "back"): String? {
Logger.step("RTSP_START_SERVER", "🎬 Starting RTSP server on port $serverPort")
try {
if (isServerRunning.get()) {
Logger.step("RTSP_ALREADY_RUNNING", "RTSP server already running")
return "rtsp://$deviceIP:$serverPort/live"
}
serverSocket = ServerSocket(serverPort)
isServerRunning.set(true)
serverThread = Thread {
while (isServerRunning.get()) {
try {
val clientSocket = serverSocket?.accept()
if (clientSocket != null) {
clientSockets.add(clientSocket)
handleRTSPClient(clientSocket)
}
} catch (e: Exception) {
if (isServerRunning.get()) {
Logger.error("RTSP_CLIENT_ERROR", "Error handling client", e)
}
}
}
}
serverThread?.start()
initializeCamera(cameraType)
val rtspUrl = "rtsp://$deviceIP:$serverPort/live"
Logger.step("RTSP_SERVER_STARTED", "✅ RTSP server started: $rtspUrl")
return rtspUrl
} catch (e: Exception) {
Logger.error("RTSP_START_ERROR", "Failed to start RTSP server", e)
return null
}
}
private fun handleRTSPClient(clientSocket: Socket) {
Thread {
try {
val input = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
val output = PrintWriter(clientSocket.getOutputStream(), true)
var line: String?
val request = StringBuilder()
// Читаем RTSP запрос
while (input.readLine().also { line = it } != null) {
request.append(line).append("\n")
if (line!!.isEmpty()) break
}
val requestStr = request.toString()
Logger.step("RTSP_REQUEST", "📡 RTSP request: ${requestStr.lines().firstOrNull()}")
when {
requestStr.contains("OPTIONS") -> {
sendRTSPResponse(output, "200 OK", mapOf(
"Public" to "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE"
))
}
requestStr.contains("DESCRIBE") -> {
val sdp = generateSDP()
sendRTSPResponse(output, "200 OK", mapOf(
"Content-Type" to "application/sdp",
"Content-Length" to sdp.length.toString()
), sdp)
}
requestStr.contains("SETUP") -> {
sendRTSPResponse(output, "200 OK", mapOf(
"Transport" to "RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001",
"Session" to "12345678"
))
}
requestStr.contains("PLAY") -> {
sendRTSPResponse(output, "200 OK", mapOf(
"Session" to "12345678"
))
startRTPStreaming(clientSocket)
}
}
} catch (e: Exception) {
Logger.error("RTSP_CLIENT_HANDLER", "Error handling RTSP client", e)
} finally {
clientSocket.close()
clientSockets.remove(clientSocket)
}
}.start()
}
private fun sendRTSPResponse(output: PrintWriter, status: String, headers: Map<String, String>, body: String = "") {
output.println("RTSP/1.0 $status")
output.println("CSeq: 1")
headers.forEach { (key, value) ->
output.println("$key: $value")
}
output.println()
if (body.isNotEmpty()) {
output.print(body)
}
output.flush()
}
private fun generateSDP(): String {
return """v=0
o=- 0 0 IN IP4 $deviceIP
s=Android Camera Stream
c=IN IP4 $deviceIP
t=0 0
m=video 9000 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 profile-level-id=42e01e
a=control:streamid=0
"""
}
private fun startRTPStreaming(clientSocket: Socket) {
Logger.step("RTSP_START_RTP", "🎥 Starting RTP streaming to client")
Thread {
try {
val rtpSocket = DatagramSocket(9000)
val clientAddress = clientSocket.inetAddress
// Симуляция RTP пакетов (в реальной реализации здесь будут кадры с камеры)
var sequenceNumber = 0
val timestamp = System.currentTimeMillis()
while (isServerRunning.get() && !clientSocket.isClosed) {
// Создаем простой RTP пакет
val rtpPacket = createRTPPacket(sequenceNumber++, timestamp, "dummy_frame".toByteArray())
val packet = DatagramPacket(rtpPacket, rtpPacket.size, clientAddress, 8000)
rtpSocket.send(packet)
Thread.sleep(33) // ~30 FPS
}
rtpSocket.close()
} catch (e: Exception) {
Logger.error("RTSP_RTP_ERROR", "Error in RTP streaming", e)
}
}.start()
}
private fun createRTPPacket(sequenceNumber: Int, timestamp: Long, payload: ByteArray): ByteArray {
val header = ByteArray(12)
// RTP Header
header[0] = 0x80.toByte() // Version 2, no padding, no extension, no CC
header[1] = 0x60.toByte() // Marker bit + Payload type (96)
// Sequence number
header[2] = (sequenceNumber shr 8).toByte()
header[3] = (sequenceNumber and 0xFF).toByte()
// Timestamp
val ts = (timestamp and 0xFFFFFFFF).toInt()
header[4] = (ts shr 24).toByte()
header[5] = (ts shr 16).toByte()
header[6] = (ts shr 8).toByte()
header[7] = (ts and 0xFF).toByte()
// SSRC (synchronization source identifier)
header[8] = 0x12.toByte()
header[9] = 0x34.toByte()
header[10] = 0x56.toByte()
header[11] = 0x78.toByte()
return header + payload
}
private fun initializeCamera(cameraType: String) {
Logger.step("RTSP_INIT_CAMERA", "📷 Initializing camera for RTSP")
try {
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraId = if (cameraType == "front") {
cameraManager.cameraIdList.find {
val characteristics = cameraManager.getCameraCharacteristics(it)
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
}
} else {
cameraManager.cameraIdList.find {
val characteristics = cameraManager.getCameraCharacteristics(it)
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK
}
} ?: cameraManager.cameraIdList.firstOrNull()
if (cameraId != null) {
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
cameraDevice = camera
Logger.step("RTSP_CAMERA_OPENED", "✅ Camera opened for RTSP")
createCaptureSession()
}
override fun onDisconnected(camera: CameraDevice) {
camera.close()
cameraDevice = null
}
override fun onError(camera: CameraDevice, error: Int) {
Logger.error("RTSP_CAMERA_ERROR", "Camera error: $error")
camera.close()
cameraDevice = null
}
}, backgroundHandler)
}
} catch (e: Exception) {
Logger.error("RTSP_CAMERA_INIT_ERROR", "Failed to initialize camera", e)
}
}
private fun createCaptureSession() {
// В реальной реализации здесь будет создание сессии захвата кадров
// и их кодирование в H.264 для передачи через RTP
Logger.step("RTSP_CAPTURE_SESSION", "📹 Camera capture session created")
}
fun switchCamera(cameraType: String) {
Logger.step("RTSP_SWITCH_CAMERA", "🔄 Switching RTSP camera to: $cameraType")
// Закрываем текущую камеру и открываем новую
cameraDevice?.close()
initializeCamera(cameraType)
}
fun stopServer() {
Logger.step("RTSP_STOP_SERVER", "🛑 Stopping RTSP server")
try {
isServerRunning.set(false)
clientSockets.forEach { it.close() }
clientSockets.clear()
serverSocket?.close()
serverSocket = null
cameraDevice?.close()
cameraDevice = null
captureSession?.close()
captureSession = null
serverThread?.interrupt()
serverThread = null
Logger.step("RTSP_SERVER_STOPPED", "✅ RTSP server stopped")
} catch (e: Exception) {
Logger.error("RTSP_STOP_ERROR", "Error stopping RTSP server", e)
}
}
fun dispose() {
stopServer()
backgroundThread?.quitSafely()
backgroundThread = null
backgroundHandler = null
}
}

View File

@@ -1,193 +0,0 @@
package com.example.godeye.streaming
import android.content.Context
import com.example.godeye.utils.Logger
import java.net.*
import java.util.concurrent.atomic.AtomicBoolean
/**
* UDP Stream Manager - прямая передача видео через UDP для минимальной задержки
* Позволяет операторам получать сырой видео поток через udp://device_ip:9999
*/
class UDPStreamManager(private val context: Context) {
private var udpSocket: DatagramSocket? = null
private var isStreaming = AtomicBoolean(false)
private var streamingThread: Thread? = null
private val streamingPort = 9999
private var deviceIP: String? = null
private var targetAddress: InetAddress? = null
private var targetPort: Int = 0
init {
detectDeviceIP()
}
private fun detectDeviceIP() {
try {
val interfaces = NetworkInterface.getNetworkInterfaces()
while (interfaces.hasMoreElements()) {
val networkInterface = interfaces.nextElement()
if (!networkInterface.isLoopback && networkInterface.isUp) {
val addresses = networkInterface.inetAddresses
while (addresses.hasMoreElements()) {
val address = addresses.nextElement()
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
deviceIP = address.hostAddress
break
}
}
}
}
} catch (e: Exception) {
Logger.error("UDP_IP_DETECTION", "Failed to detect IP", e)
}
}
fun startStreaming(cameraType: String = "back"): String? {
Logger.step("UDP_START_STREAMING", "🎬 Starting UDP streaming on port $streamingPort")
try {
if (isStreaming.get()) {
Logger.step("UDP_ALREADY_STREAMING", "UDP streaming already active")
return "udp://$deviceIP:$streamingPort"
}
udpSocket = DatagramSocket(streamingPort)
isStreaming.set(true)
startFrameStreaming(cameraType)
val udpUrl = "udp://$deviceIP:$streamingPort"
Logger.step("UDP_STREAMING_STARTED", "✅ UDP streaming started: $udpUrl")
return udpUrl
} catch (e: Exception) {
Logger.error("UDP_START_ERROR", "Failed to start UDP streaming", e)
return null
}
}
private fun startFrameStreaming(cameraType: String) {
streamingThread = Thread {
Logger.step("UDP_FRAME_STREAMING", "🎥 Starting UDP frame streaming")
var frameCounter = 0
while (isStreaming.get()) {
try {
// В реальной реализации здесь будут кадры с камеры
val frameData = generateDummyFrame(frameCounter++, cameraType)
// Если есть подключенные клиенты, отправляем им кадры
if (targetAddress != null && targetPort > 0) {
sendFrame(frameData)
}
// ~30 FPS
Thread.sleep(33)
} catch (e: InterruptedException) {
break
} catch (e: Exception) {
Logger.error("UDP_FRAME_ERROR", "Error streaming frame", e)
}
}
}
streamingThread?.start()
}
private fun generateDummyFrame(frameNumber: Int, cameraType: String): ByteArray {
// Заглушка - в реальной реализации здесь будут сжатые кадры H.264
val frameHeader = ByteArray(8)
// Frame header: magic number + frame number + camera type
frameHeader[0] = 0x47.toByte() // Magic 'G'
frameHeader[1] = 0x45.toByte() // Magic 'E'
frameHeader[2] = (frameNumber shr 24).toByte()
frameHeader[3] = (frameNumber shr 16).toByte()
frameHeader[4] = (frameNumber shr 8).toByte()
frameHeader[5] = (frameNumber and 0xFF).toByte()
frameHeader[6] = if (cameraType == "back") 0x00 else 0x01
frameHeader[7] = 0x00 // Reserved
val frameData = "FRAME_${frameNumber}_${cameraType}_${System.currentTimeMillis()}".toByteArray()
return frameHeader + frameData
}
private fun sendFrame(frameData: ByteArray) {
try {
val packet = DatagramPacket(
frameData,
frameData.size,
targetAddress,
targetPort
)
udpSocket?.send(packet)
} catch (e: Exception) {
Logger.error("UDP_SEND_FRAME", "Error sending UDP frame", e)
}
}
/**
* Устанавливает адрес клиента для отправки кадров
*/
fun setClient(clientIP: String, clientPort: Int) {
try {
targetAddress = InetAddress.getByName(clientIP)
targetPort = clientPort
Logger.step("UDP_CLIENT_SET", "📡 UDP client set: $clientIP:$clientPort")
} catch (e: Exception) {
Logger.error("UDP_SET_CLIENT", "Error setting UDP client", e)
}
}
/**
* Получение информации о UDP стриме для отправки клиенту
*/
fun getStreamInfo(): Map<String, Any> {
return mapOf(
"protocol" to "udp",
"ip" to (deviceIP ?: "unknown"),
"port" to streamingPort,
"url" to "udp://$deviceIP:$streamingPort",
"format" to "raw_frames",
"fps" to 30,
"active" to isStreaming.get()
)
}
fun switchCamera(cameraType: String) {
Logger.step("UDP_SWITCH_CAMERA", "🔄 Switching UDP camera to: $cameraType")
// В реальной реализации здесь будет переключение источника кадров
// Новый тип камеры будет включен в следующие кадры
}
fun stopStreaming() {
Logger.step("UDP_STOP_STREAMING", "🛑 Stopping UDP streaming")
try {
isStreaming.set(false)
streamingThread?.interrupt()
streamingThread = null
udpSocket?.close()
udpSocket = null
targetAddress = null
targetPort = 0
Logger.step("UDP_STREAMING_STOPPED", "✅ UDP streaming stopped")
} catch (e: Exception) {
Logger.error("UDP_STOP_ERROR", "Error stopping UDP streaming", e)
}
}
fun dispose() {
stopStreaming()
}
}

View File

@@ -1,355 +0,0 @@
package com.example.godeye.streaming
import android.content.Context
import android.hardware.camera2.CameraManager
import com.example.godeye.utils.Logger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONObject
import java.net.NetworkInterface
import java.net.SocketException
/**
* Unified Streaming Manager - управляет различными протоколами прямой передачи видео
*
* Поддерживаемые протоколы:
* 1. WebRTC P2P - для веб-браузеров
* 2. RTSP Server - для специализированных клиентов
* 3. HTTP Live Streaming (HLS) - для универсальной совместимости
* 4. Raw UDP Stream - для минимальной задержки
*/
class UnifiedStreamingManager(
private val context: Context,
private val onSignalingMessage: (message: JSONObject) -> Unit
) {
// Состояния стриминга
private val _streamingState = MutableStateFlow(StreamingState.STOPPED)
val streamingState: StateFlow<StreamingState> = _streamingState.asStateFlow()
private val _availableProtocols = MutableStateFlow<List<StreamingProtocol>>(emptyList())
val availableProtocols: StateFlow<List<StreamingProtocol>> = _availableProtocols.asStateFlow()
private val _activeStreams = MutableStateFlow<Map<String, StreamInfo>>(emptyMap())
val activeStreams: StateFlow<Map<String, StreamInfo>> = _activeStreams.asStateFlow()
// Менеджеры протоколов
private var webRTCManager: WebRTCStreamManager? = null
private var rtspManager: RTSPStreamManager? = null
private var hlsManager: HLSStreamManager? = null
private var udpManager: UDPStreamManager? = null
private var deviceIP: String? = null
init {
Logger.step("UNIFIED_STREAMING_INIT", "🎬 Initializing Unified Streaming Manager")
detectDeviceIP()
initializeProtocolSupport()
}
/**
* Определение IP адреса устройства для прямых соединений
*/
private fun detectDeviceIP() {
try {
val interfaces = NetworkInterface.getNetworkInterfaces()
while (interfaces.hasMoreElements()) {
val networkInterface = interfaces.nextElement()
if (!networkInterface.isLoopback && networkInterface.isUp) {
val addresses = networkInterface.inetAddresses
while (addresses.hasMoreElements()) {
val address = addresses.nextElement()
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
deviceIP = address.hostAddress
Logger.step("DEVICE_IP_DETECTED", "📍 Device IP: $deviceIP")
break
}
}
}
}
} catch (e: SocketException) {
Logger.error("IP_DETECTION_ERROR", "Failed to detect device IP", e)
}
}
/**
* Инициализация поддержки различных протоколов
*/
private fun initializeProtocolSupport() {
val supportedProtocols = mutableListOf<StreamingProtocol>()
try {
// WebRTC поддержка
webRTCManager = WebRTCStreamManager(context, onSignalingMessage)
supportedProtocols.add(
StreamingProtocol(
type = "webrtc",
name = "WebRTC P2P",
description = "Прямое P2P соединение для веб-браузеров",
isSupported = true,
connectionInfo = "Автоматическое P2P соединение"
)
)
Logger.step("WEBRTC_SUPPORT", "✅ WebRTC protocol supported")
} catch (e: Exception) {
Logger.error("WEBRTC_SUPPORT_ERROR", "WebRTC not supported", e)
}
try {
// RTSP Server поддержка
rtspManager = RTSPStreamManager(context)
supportedProtocols.add(
StreamingProtocol(
type = "rtsp",
name = "RTSP Server",
description = "RTSP сервер для специализированных клиентов",
isSupported = true,
connectionInfo = "rtsp://$deviceIP:8554/live"
)
)
Logger.step("RTSP_SUPPORT", "✅ RTSP protocol supported")
} catch (e: Exception) {
Logger.error("RTSP_SUPPORT_ERROR", "RTSP not supported", e)
}
try {
// HLS поддержка
hlsManager = HLSStreamManager()
supportedProtocols.add(
StreamingProtocol(
type = "hls",
name = "HTTP Live Streaming",
description = "HLS стрим для универсальной совместимости",
isSupported = true,
connectionInfo = "http://$deviceIP:8080/hls/stream.m3u8"
)
)
Logger.step("HLS_SUPPORT", "✅ HLS protocol supported")
} catch (e: Exception) {
Logger.error("HLS_SUPPORT_ERROR", "HLS not supported", e)
}
try {
// UDP Raw Stream поддержка
udpManager = UDPStreamManager(context)
supportedProtocols.add(
StreamingProtocol(
type = "udp",
name = "Raw UDP Stream",
description = "Прямой UDP поток для минимальной задержки",
isSupported = true,
connectionInfo = "udp://$deviceIP:9999"
)
)
Logger.step("UDP_SUPPORT", "✅ UDP protocol supported")
} catch (e: Exception) {
Logger.error("UDP_SUPPORT_ERROR", "UDP not supported", e)
}
_availableProtocols.value = supportedProtocols
Logger.step("PROTOCOLS_INITIALIZED", "🎯 Initialized ${supportedProtocols.size} streaming protocols")
}
/**
* Запуск стриминга с выбранными протоколами
*/
fun startStreaming(
sessionId: String,
requestedProtocols: List<String> = listOf("webrtc", "rtsp"),
cameraType: String = "back"
) {
Logger.step("START_STREAMING", "🎬 Starting streaming for session: $sessionId")
Logger.step("STREAMING_PROTOCOLS", "📡 Requested protocols: ${requestedProtocols.joinToString(", ")}")
_streamingState.value = StreamingState.STARTING
val activeStreams = mutableMapOf<String, StreamInfo>()
try {
// Запуск WebRTC если запрошен
if ("webrtc" in requestedProtocols && webRTCManager != null) {
webRTCManager?.startStreaming(sessionId, cameraType)
activeStreams["webrtc"] = StreamInfo(
protocol = "webrtc",
sessionId = sessionId,
isActive = true,
connectionUrl = "P2P Connection",
startTime = System.currentTimeMillis()
)
Logger.step("WEBRTC_STARTED", "✅ WebRTC streaming started")
}
// Запуск RTSP если запрошен
if ("rtsp" in requestedProtocols && rtspManager != null) {
val rtspUrl = rtspManager?.startServer(cameraType)
if (rtspUrl != null) {
activeStreams["rtsp"] = StreamInfo(
protocol = "rtsp",
sessionId = sessionId,
isActive = true,
connectionUrl = rtspUrl,
startTime = System.currentTimeMillis()
)
Logger.step("RTSP_STARTED", "✅ RTSP streaming started: $rtspUrl")
}
}
// Запуск HLS если запрошен
if ("hls" in requestedProtocols && hlsManager != null) {
val hlsUrl = hlsManager?.startStreaming(cameraType)
if (hlsUrl != null) {
activeStreams["hls"] = StreamInfo(
protocol = "hls",
sessionId = sessionId,
isActive = true,
connectionUrl = hlsUrl,
startTime = System.currentTimeMillis()
)
Logger.step("HLS_STARTED", "✅ HLS streaming started: $hlsUrl")
}
}
// Запуск UDP если запрошен
if ("udp" in requestedProtocols && udpManager != null) {
val udpUrl = udpManager?.startStreaming(cameraType)
if (udpUrl != null) {
activeStreams["udp"] = StreamInfo(
protocol = "udp",
sessionId = sessionId,
isActive = true,
connectionUrl = udpUrl,
startTime = System.currentTimeMillis()
)
Logger.step("UDP_STARTED", "✅ UDP streaming started: $udpUrl")
}
}
_activeStreams.value = activeStreams
_streamingState.value = if (activeStreams.isNotEmpty()) StreamingState.ACTIVE else StreamingState.ERROR
// Отправляем информацию о доступных стримах оператору через сигнальный сервер
sendStreamingInfo(sessionId, activeStreams)
} catch (e: Exception) {
Logger.error("START_STREAMING_ERROR", "Failed to start streaming", e)
_streamingState.value = StreamingState.ERROR
}
}
/**
* Остановка всех стримов
*/
fun stopStreaming(sessionId: String) {
Logger.step("STOP_STREAMING", "🛑 Stopping streaming for session: $sessionId")
try {
webRTCManager?.stopStreaming()
rtspManager?.stopServer()
hlsManager?.stopStreaming()
udpManager?.stopStreaming()
_activeStreams.value = emptyMap()
_streamingState.value = StreamingState.STOPPED
Logger.step("STREAMING_STOPPED", "✅ All streaming stopped")
} catch (e: Exception) {
Logger.error("STOP_STREAMING_ERROR", "Error stopping streaming", e)
}
}
/**
* Переключение камеры во всех активных стримах
*/
fun switchCamera(cameraType: String) {
Logger.step("SWITCH_CAMERA", "🔄 Switching camera to: $cameraType")
try {
webRTCManager?.switchCamera(cameraType)
rtspManager?.switchCamera(cameraType)
hlsManager?.switchCamera(cameraType)
udpManager?.switchCamera(cameraType)
Logger.step("CAMERA_SWITCHED", "✅ Camera switched to: $cameraType")
} catch (e: Exception) {
Logger.error("SWITCH_CAMERA_ERROR", "Error switching camera", e)
}
}
/**
* Отправка информации о доступных стримах оператору
*/
private fun sendStreamingInfo(sessionId: String, streams: Map<String, StreamInfo>) {
val streamingInfo = JSONObject().apply {
put("type", "streaming_info")
put("sessionId", sessionId)
put("deviceIP", deviceIP)
put("streams", JSONObject().apply {
streams.forEach { (protocol, info) ->
put(protocol, JSONObject().apply {
put("url", info.connectionUrl)
put("active", info.isActive)
put("protocol", info.protocol)
})
}
})
}
onSignalingMessage(streamingInfo)
Logger.step("STREAMING_INFO_SENT", "📡 Streaming info sent to operator")
}
/**
* Получение статистики стриминга
*/
fun getStreamingStats(): Map<String, Any> {
val stats = mutableMapOf<String, Any>()
stats["state"] = _streamingState.value.name
stats["activeStreams"] = _activeStreams.value.size
stats["deviceIP"] = deviceIP ?: "unknown"
stats["supportedProtocols"] = _availableProtocols.value.map { it.type }
_activeStreams.value.forEach { (protocol, info) ->
stats["${protocol}_uptime"] = (System.currentTimeMillis() - info.startTime) / 1000
}
return stats
}
fun dispose() {
Logger.step("UNIFIED_STREAMING_DISPOSE", "🧹 Disposing Unified Streaming Manager")
try {
stopStreaming("dispose")
webRTCManager?.dispose()
rtspManager?.dispose()
hlsManager?.dispose()
udpManager?.dispose()
} catch (e: Exception) {
Logger.error("DISPOSE_ERROR", "Error during disposal", e)
}
}
}
// Данные классы для стриминга
enum class StreamingState {
STOPPED, STARTING, ACTIVE, ERROR
}
data class StreamingProtocol(
val type: String,
val name: String,
val description: String,
val isSupported: Boolean,
val connectionInfo: String
)
data class StreamInfo(
val protocol: String,
val sessionId: String,
val isActive: Boolean,
val connectionUrl: String,
val startTime: Long
)

View File

@@ -1,218 +0,0 @@
package com.example.godeye.streaming
import android.content.Context
import com.example.godeye.utils.Logger
import org.json.JSONObject
import org.webrtc.*
import org.webrtc.audio.JavaAudioDeviceModule
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* WebRTC Stream Manager - улучшенная версия для прямого P2P соединения
*/
class WebRTCStreamManager(
private val context: Context,
private val onSignalingMessage: (message: JSONObject) -> Unit
) {
private var peerConnectionFactory: PeerConnectionFactory? = null
private var peerConnection: PeerConnection? = null
private var localVideoTrack: VideoTrack? = null
private var localAudioTrack: AudioTrack? = null
private var videoCapturer: CameraVideoCapturer? = null
private var surfaceTextureHelper: SurfaceTextureHelper? = null
private val _connectionState = MutableStateFlow(PeerConnection.PeerConnectionState.NEW)
val connectionState: StateFlow<PeerConnection.PeerConnectionState> = _connectionState.asStateFlow()
private val _isStreaming = MutableStateFlow(false)
val isStreaming: StateFlow<Boolean> = _isStreaming.asStateFlow()
private val iceServers = listOf(
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()
)
init {
initializePeerConnectionFactory()
}
private fun initializePeerConnectionFactory() {
val initOptions = PeerConnectionFactory.InitializationOptions.builder(context)
.setEnableInternalTracer(true)
.createInitializationOptions()
PeerConnectionFactory.initialize(initOptions)
val audioDeviceModule = JavaAudioDeviceModule.builder(context).createAudioDeviceModule()
peerConnectionFactory = PeerConnectionFactory.builder()
.setAudioDeviceModule(audioDeviceModule)
.setVideoEncoderFactory(DefaultVideoEncoderFactory(
EglBase.create().eglBaseContext, true, true))
.setVideoDecoderFactory(DefaultVideoDecoderFactory(EglBase.create().eglBaseContext))
.createPeerConnectionFactory()
Logger.step("WEBRTC_FACTORY_READY", "✅ WebRTC PeerConnectionFactory initialized")
}
fun startStreaming(sessionId: String, cameraType: String = "back") {
Logger.step("WEBRTC_START_STREAMING", "🎬 Starting WebRTC streaming for session: $sessionId")
try {
createPeerConnection()
initializeLocalMedia(cameraType)
createOffer(sessionId)
_isStreaming.value = true
} catch (e: Exception) {
Logger.error("WEBRTC_START_ERROR", "Failed to start WebRTC streaming", e)
}
}
fun stopStreaming() {
Logger.step("WEBRTC_STOP_STREAMING", "🛑 Stopping WebRTC streaming")
try {
videoCapturer?.stopCapture()
videoCapturer?.dispose()
localVideoTrack?.dispose()
localAudioTrack?.dispose()
peerConnection?.close()
peerConnection = null
_isStreaming.value = false
_connectionState.value = PeerConnection.PeerConnectionState.CLOSED
} catch (e: Exception) {
Logger.error("WEBRTC_STOP_ERROR", "Error stopping WebRTC", e)
}
}
fun switchCamera(cameraType: String) {
Logger.step("WEBRTC_SWITCH_CAMERA", "🔄 Switching WebRTC camera to: $cameraType")
(videoCapturer as? CameraVideoCapturer)?.switchCamera(null)
}
private fun createPeerConnection() {
val config = PeerConnection.RTCConfiguration(iceServers).apply {
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
}
peerConnection = peerConnectionFactory?.createPeerConnection(config, object : PeerConnection.Observer {
override fun onSignalingChange(state: PeerConnection.SignalingState) {
Logger.step("WEBRTC_SIGNALING", "Signaling state: $state")
}
override fun onIceConnectionChange(state: PeerConnection.IceConnectionState) {
Logger.step("WEBRTC_ICE_STATE", "ICE state: $state")
}
override fun onConnectionChange(state: PeerConnection.PeerConnectionState) {
_connectionState.value = state
Logger.step("WEBRTC_CONNECTION_STATE", "Connection state: $state")
}
override fun onIceCandidate(candidate: IceCandidate) {
val candidateMsg = JSONObject().apply {
put("type", "ice-candidate")
put("candidate", candidate.sdp)
put("sdpMLineIndex", candidate.sdpMLineIndex)
put("sdpMid", candidate.sdpMid)
}
onSignalingMessage(candidateMsg)
}
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {}
override fun onAddStream(stream: MediaStream) {}
override fun onRemoveStream(stream: MediaStream) {}
override fun onDataChannel(channel: DataChannel) {}
override fun onRenegotiationNeeded() {}
override fun onIceGatheringChange(state: PeerConnection.IceGatheringState) {}
override fun onIceConnectionReceivingChange(receiving: Boolean) {}
})
}
private fun initializeLocalMedia(cameraType: String) {
val videoSource = peerConnectionFactory?.createVideoSource(false)
localVideoTrack = peerConnectionFactory?.createVideoTrack("video", videoSource)
val audioSource = peerConnectionFactory?.createAudioSource(MediaConstraints())
localAudioTrack = peerConnectionFactory?.createAudioTrack("audio", audioSource)
val stream = peerConnectionFactory?.createLocalMediaStream("stream")
localVideoTrack?.let { stream?.addTrack(it) }
localAudioTrack?.let { stream?.addTrack(it) }
stream?.let { peerConnection?.addStream(it) }
initializeCamera(videoSource, cameraType)
}
private fun initializeCamera(videoSource: VideoSource?, cameraType: String) {
val cameraEnumerator = Camera2Enumerator(context)
val cameraName = if (cameraType == "front") {
cameraEnumerator.deviceNames.find { cameraEnumerator.isFrontFacing(it) }
} else {
cameraEnumerator.deviceNames.find { cameraEnumerator.isBackFacing(it) }
} ?: cameraEnumerator.deviceNames.firstOrNull()
if (cameraName != null) {
surfaceTextureHelper = SurfaceTextureHelper.create("CameraThread", EglBase.create().eglBaseContext)
videoCapturer = cameraEnumerator.createCapturer(cameraName, null) as? CameraVideoCapturer
videoCapturer?.initialize(surfaceTextureHelper, context, videoSource?.capturerObserver)
videoCapturer?.startCapture(1280, 720, 30)
}
}
private fun createOffer(sessionId: String) {
val constraints = MediaConstraints()
peerConnection?.createOffer(object : SdpObserver {
override fun onCreateSuccess(desc: SessionDescription) {
peerConnection?.setLocalDescription(object : SdpObserver {
override fun onSetSuccess() {
val offerMsg = JSONObject().apply {
put("type", "offer")
put("sessionId", sessionId)
put("sdp", desc.description)
}
onSignalingMessage(offerMsg)
}
override fun onSetFailure(error: String) {}
override fun onCreateSuccess(p0: SessionDescription?) {}
override fun onCreateFailure(p0: String?) {}
}, desc)
}
override fun onCreateFailure(error: String) {}
override fun onSetSuccess() {}
override fun onSetFailure(error: String) {}
}, constraints)
}
fun handleAnswer(answerSdp: String) {
val desc = SessionDescription(SessionDescription.Type.ANSWER, answerSdp)
peerConnection?.setRemoteDescription(object : SdpObserver {
override fun onSetSuccess() {
Logger.step("WEBRTC_ANSWER_SET", "✅ WebRTC answer processed")
}
override fun onSetFailure(error: String) {}
override fun onCreateSuccess(p0: SessionDescription?) {}
override fun onCreateFailure(p0: String?) {}
}, desc)
}
fun handleIceCandidate(candidate: String, sdpMLineIndex: Int, sdpMid: String) {
val iceCandidate = IceCandidate(sdpMid, sdpMLineIndex, candidate)
peerConnection?.addIceCandidate(iceCandidate)
}
fun dispose() {
stopStreaming()
peerConnectionFactory?.dispose()
}
}

View File

@@ -0,0 +1,338 @@
package com.example.godeye.ui
import android.os.Bundle
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.example.godeye.R
import com.example.godeye.managers.ConnectionManager
import com.example.godeye.utils.Logger
import com.example.godeye.utils.PreferenceManager
import kotlinx.coroutines.launch
import android.widget.*
import androidx.core.content.ContextCompat
/**
* Активность для мониторинга видеопотока без предварительного просмотра
* Показывает только статус трансляции и счетчик времени записи
*/
class StreamingMonitorActivity : ComponentActivity() {
private lateinit var connectionManager: ConnectionManager
private lateinit var preferenceManager: PreferenceManager
// UI элементы
private lateinit var statusText: TextView
private lateinit var recordingTimeText: TextView
private lateinit var streamingStatusIndicator: View
private lateinit var startButton: Button
private lateinit var stopButton: Button
private lateinit var switchCameraButton: Button
private lateinit var disconnectButton: Button
private lateinit var recordingIndicator: ImageView
private lateinit var connectionStatusText: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_streaming_monitor)
initializeUI()
initializeManagers()
setupEventListeners()
setupObservers()
}
/**
* Инициализация UI элементов
*/
private fun initializeUI() {
statusText = findViewById(R.id.statusText)
recordingTimeText = findViewById(R.id.recordingTimeText)
streamingStatusIndicator = findViewById(R.id.streamingStatusIndicator)
startButton = findViewById(R.id.startButton)
stopButton = findViewById(R.id.stopButton)
switchCameraButton = findViewById(R.id.switchCameraButton)
disconnectButton = findViewById(R.id.disconnectButton)
recordingIndicator = findViewById(R.id.recordingIndicator)
connectionStatusText = findViewById(R.id.connectionStatusText)
// Начальное состояние
updateUIForStatus(ConnectionManager.StreamingStatus.IDLE)
recordingTimeText.text = "00:00:00"
connectionStatusText.text = "Не подключено"
}
/**
* Инициализация менеджеров
*/
private fun initializeManagers() {
preferenceManager = PreferenceManager(this)
connectionManager = ConnectionManager(this, preferenceManager)
}
/**
* Настройка слушателей событий
*/
private fun setupEventListeners() {
startButton.setOnClickListener {
startStreaming()
}
stopButton.setOnClickListener {
stopStreaming()
}
switchCameraButton.setOnClickListener {
switchCamera()
}
disconnectButton.setOnClickListener {
disconnect()
}
}
/**
* Настройка наблюдателей за состоянием
*/
private fun setupObservers() {
// Наблюдение за событиями соединения
lifecycleScope.launch {
connectionManager.events.collect { event ->
when (event) {
is ConnectionManager.ConnectionEvent.Connected -> {
runOnUiThread {
connectionStatusText.text = "Подключено к серверу"
connectionStatusText.setTextColor(ContextCompat.getColor(this@StreamingMonitorActivity, R.color.success_green))
Toast.makeText(this@StreamingMonitorActivity, "Подключено к серверу", Toast.LENGTH_SHORT).show()
}
}
is ConnectionManager.ConnectionEvent.Disconnected -> {
runOnUiThread {
connectionStatusText.text = "Отключено от сервера"
connectionStatusText.setTextColor(ContextCompat.getColor(this@StreamingMonitorActivity, R.color.error_red))
}
}
is ConnectionManager.ConnectionEvent.Error -> {
runOnUiThread {
Toast.makeText(this@StreamingMonitorActivity, "Ошибка: ${event.message}", Toast.LENGTH_LONG).show()
Logger.error("STREAMING_MONITOR_ERROR", event.message, null)
}
}
is ConnectionManager.ConnectionEvent.StatusUpdated -> {
runOnUiThread {
statusText.text = event.status
}
}
is ConnectionManager.ConnectionEvent.RecordingTimeUpdated -> {
runOnUiThread {
recordingTimeText.text = event.duration
}
}
is ConnectionManager.ConnectionEvent.StreamingStarted -> {
runOnUiThread {
Toast.makeText(this@StreamingMonitorActivity, "Трансляция началась", Toast.LENGTH_SHORT).show()
}
}
is ConnectionManager.ConnectionEvent.StreamingStopped -> {
runOnUiThread {
Toast.makeText(this@StreamingMonitorActivity, "Трансляция остановлена", Toast.LENGTH_SHORT).show()
}
}
null -> {}
}
}
}
// Наблюдение за статусом трансляции
lifecycleScope.launch {
connectionManager.streamingStatus.collect { status ->
runOnUiThread {
updateUIForStatus(status)
}
}
}
// Наблюдение за длительностью записи
lifecycleScope.launch {
connectionManager.recordingDuration.collect { duration ->
runOnUiThread {
recordingTimeText.text = duration
}
}
}
// Наблюдение за состоянием записи
lifecycleScope.launch {
connectionManager.isRecording.collect { isRecording ->
runOnUiThread {
updateRecordingIndicator(isRecording)
}
}
}
}
/**
* Обновление UI в зависимости от статуса трансляции
*/
private fun updateUIForStatus(status: ConnectionManager.StreamingStatus) {
when (status) {
ConnectionManager.StreamingStatus.IDLE -> {
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_idle))
startButton.isEnabled = true
stopButton.isEnabled = false
switchCameraButton.isEnabled = false
statusText.text = "Готов к трансляции"
}
ConnectionManager.StreamingStatus.CONNECTING -> {
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_connecting))
startButton.isEnabled = false
stopButton.isEnabled = false
switchCameraButton.isEnabled = false
statusText.text = "Подключение..."
}
ConnectionManager.StreamingStatus.STREAMING -> {
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_streaming))
startButton.isEnabled = false
stopButton.isEnabled = true
switchCameraButton.isEnabled = true
statusText.text = "Трансляция активна"
}
ConnectionManager.StreamingStatus.ERROR -> {
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_error))
startButton.isEnabled = true
stopButton.isEnabled = false
switchCameraButton.isEnabled = false
statusText.text = "Ошибка трансляции"
}
ConnectionManager.StreamingStatus.STOPPING -> {
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_stopping))
startButton.isEnabled = false
stopButton.isEnabled = false
switchCameraButton.isEnabled = false
statusText.text = "Остановка трансляции..."
}
}
}
/**
* Обновление индикатора записи
*/
private fun updateRecordingIndicator(isRecording: Boolean) {
if (isRecording) {
recordingIndicator.setImageResource(R.drawable.ic_recording_active)
recordingIndicator.visibility = View.VISIBLE
// Добавляем анимацию мигания
recordingIndicator.animate()
.alpha(0f)
.setDuration(500)
.withEndAction {
recordingIndicator.animate()
.alpha(1f)
.setDuration(500)
.withEndAction {
if (connectionManager.isCurrentlyRecording()) {
updateRecordingIndicator(true) // Повторяем анимацию
}
}
}
} else {
recordingIndicator.visibility = View.GONE
recordingIndicator.clearAnimation()
}
}
/**
* Начало трансляции
*/
private fun startStreaming() {
try {
Logger.step("MONITOR_START_STREAMING", "Starting streaming from monitor")
// Здесь можно получить sessionId и operatorId из Intent или диалога
val sessionId = intent.getStringExtra("sessionId") ?: generateSessionId()
val operatorId = intent.getStringExtra("operatorId") ?: "operator_1"
val cameraType = intent.getStringExtra("cameraType") ?: "back"
connectionManager.startVideoStream(sessionId, operatorId, cameraType)
} catch (e: Exception) {
Logger.error("MONITOR_START_ERROR", "Failed to start streaming", e)
Toast.makeText(this, "Ошибка запуска трансляции: ${e.message}", Toast.LENGTH_LONG).show()
}
}
/**
* Остановка трансляции
*/
private fun stopStreaming() {
try {
Logger.step("MONITOR_STOP_STREAMING", "Stopping streaming from monitor")
connectionManager.stopAllStreaming()
} catch (e: Exception) {
Logger.error("MONITOR_STOP_ERROR", "Failed to stop streaming", e)
Toast.makeText(this, "Ошибка остановки трансляции: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
/**
* Переключение камеры
*/
private fun switchCamera() {
try {
Logger.step("MONITOR_SWITCH_CAMERA", "Switching camera from monitor")
connectionManager.switchCamera()
} catch (e: Exception) {
Logger.error("MONITOR_SWITCH_ERROR", "Failed to switch camera", e)
Toast.makeText(this, "Ошибка переключения камеры: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
/**
* Отключение от сервера
*/
private fun disconnect() {
try {
Logger.step("MONITOR_DISCONNECT", "Disconnecting from monitor")
connectionManager.disconnect()
} catch (e: Exception) {
Logger.error("MONITOR_DISCONNECT_ERROR", "Failed to disconnect", e)
}
}
/**
* Генерация ID сессии
*/
private fun generateSessionId(): String {
return "session_${System.currentTimeMillis()}"
}
override fun onDestroy() {
super.onDestroy()
try {
connectionManager.dispose()
} catch (e: Exception) {
Logger.error("MONITOR_DISPOSE_ERROR", "Failed to dispose connection manager", e)
}
}
override fun onPause() {
super.onPause()
// Не останавливаем трансляцию при уходе активности в фон
Logger.step("MONITOR_PAUSE", "Monitor activity paused, streaming continues")
}
override fun onResume() {
super.onResume()
Logger.step("MONITOR_RESUME", "Monitor activity resumed")
// Обновляем UI с текущим состоянием
runOnUiThread {
updateUIForStatus(connectionManager.getCurrentStreamingStatus())
recordingTimeText.text = connectionManager.getCurrentRecordingDuration()
updateRecordingIndicator(connectionManager.isCurrentlyRecording())
}
}
}

View File

@@ -0,0 +1,267 @@
package com.example.godeye.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.godeye.utils.Logger
/**
* Компонент для отображения детальной информации о WebRTC соединениях
*/
@Composable
fun NetworkTrafficMonitor(
sessionId: String?,
modifier: Modifier = Modifier
) {
// Состояние для отслеживания логов
var networkLogs by remember { mutableStateOf(listOf<NetworkLogEntry>()) }
var isExpanded by remember { mutableStateOf(false) }
// Подписываемся на логи (в реальном приложении это бы было через Flow)
LaunchedEffect(sessionId) {
// Здесь мы бы подписались на логи, но для демонстрации создадим тестовые данные
networkLogs = listOf(
NetworkLogEntry("WEBRTC_ICE_SERVERS", "ICE servers: stun:stun.l.google.com:19302", System.currentTimeMillis()),
NetworkLogEntry("WEBRTC_SDP_CANDIDATE", "UDP candidate: 192.168.1.100:54321", System.currentTimeMillis()),
NetworkLogEntry("WEBRTC_CONNECTION_STATE", "ICE connection state: CONNECTED", System.currentTimeMillis()),
NetworkLogEntry("WEBRTC_VIDEO_STATS", "Sending video to remote peer", System.currentTimeMillis())
)
}
Card(
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1A2B3D).copy(alpha = 0.9f)
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
// Заголовок с кнопкой раскрытия
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = Icons.Default.NetworkCheck,
contentDescription = null,
tint = Color(0xFF00BFFF),
modifier = Modifier.size(24.dp)
)
Text(
text = "📡 Мониторинг трафика",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF00BFFF)
)
}
IconButton(
onClick = { isExpanded = !isExpanded }
) {
Icon(
imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (isExpanded) "Скрыть" else "Показать",
tint = Color(0xFF00BFFF)
)
}
}
if (isExpanded) {
Spacer(modifier = Modifier.height(12.dp))
// Информация о текущем соединении
if (sessionId != null) {
ConnectionSummary(sessionId = sessionId)
Spacer(modifier = Modifier.height(12.dp))
}
// Логи сетевой активности
Text(
text = "Сетевые логи:",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFFE0F4FF)
)
Spacer(modifier = Modifier.height(8.dp))
LazyColumn(
modifier = Modifier.heightIn(max = 200.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(networkLogs.reversed()) { log ->
NetworkLogItem(log = log)
}
}
} else {
// Краткая информация когда свернуто
Spacer(modifier = Modifier.height(8.dp))
if (sessionId != null) {
Text(
text = "Активная сессия: $sessionId",
fontSize = 14.sp,
color = Color(0xFF40E0D0)
)
Text(
text = "Статус: Передача видео активна",
fontSize = 12.sp,
color = Color(0xFFB0C4DE)
)
} else {
Text(
text = "Нет активных соединений",
fontSize = 14.sp,
color = Color(0xFFB0C4DE)
)
}
}
}
}
}
@Composable
private fun ConnectionSummary(sessionId: String) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2D4A73).copy(alpha = 0.7f)
)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Сессия:",
fontSize = 12.sp,
color = Color(0xFFB0C4DE)
)
Text(
text = sessionId,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFFE0F4FF)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Протокол:",
fontSize = 12.sp,
color = Color(0xFFB0C4DE)
)
Text(
text = "WebRTC (UDP/STUN)",
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFFE0F4FF)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Назначение:",
fontSize = 12.sp,
color = Color(0xFFB0C4DE)
)
Text(
text = "Удаленный оператор",
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF40E0D0)
)
}
}
}
}
@Composable
private fun NetworkLogItem(log: NetworkLogEntry) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
Color(0xFF0A1828).copy(alpha = 0.5f),
RoundedCornerShape(4.dp)
)
.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Индикатор типа лога
val (icon, color) = when {
log.type.contains("CANDIDATE") -> Icons.Default.LocationOn to Color(0xFF4CAF50)
log.type.contains("CONNECTION") -> Icons.Default.Link to Color(0xFF2196F3)
log.type.contains("STATS") -> Icons.Default.Analytics to Color(0xFFFF9800)
log.type.contains("SDP") -> Icons.Default.Code to Color(0xFF9C27B0)
else -> Icons.Default.Info to Color(0xFF607D8B)
}
Icon(
imageVector = icon,
contentDescription = null,
tint = color,
modifier = Modifier.size(16.dp)
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = log.type,
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = color
)
Text(
text = log.message,
fontSize = 11.sp,
color = Color(0xFFE0F4FF),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
// Временная метка
Text(
text = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date(log.timestamp)),
fontSize = 10.sp,
color = Color(0xFFB0C4DE)
)
}
}
data class NetworkLogEntry(
val type: String,
val message: String,
val timestamp: Long
)

View File

@@ -0,0 +1,251 @@
package com.example.godeye.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.example.godeye.models.IncomingCall
import com.example.godeye.models.SessionState
import com.example.godeye.models.SignalingState
@Composable
fun SignalingStatusCard(
signalingState: SignalingState,
sessionState: SessionState,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = when (signalingState) {
SignalingState.CONNECTED -> MaterialTheme.colorScheme.primaryContainer
SignalingState.CONNECTING, SignalingState.RECONNECTING -> MaterialTheme.colorScheme.secondaryContainer
SignalingState.ERROR -> MaterialTheme.colorScheme.errorContainer
SignalingState.DISCONNECTED -> MaterialTheme.colorScheme.surfaceVariant
}
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = when (signalingState) {
SignalingState.CONNECTED -> Icons.Default.CheckCircle
SignalingState.CONNECTING, SignalingState.RECONNECTING -> Icons.Default.Refresh
SignalingState.ERROR -> Icons.Default.Warning
SignalingState.DISCONNECTED -> Icons.Default.Close
},
contentDescription = null,
tint = when (signalingState) {
SignalingState.CONNECTED -> MaterialTheme.colorScheme.primary
SignalingState.CONNECTING, SignalingState.RECONNECTING -> MaterialTheme.colorScheme.secondary
SignalingState.ERROR -> MaterialTheme.colorScheme.error
SignalingState.DISCONNECTED -> MaterialTheme.colorScheme.onSurfaceVariant
},
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = when (signalingState) {
SignalingState.CONNECTED -> "Подключено к серверу"
SignalingState.CONNECTING -> "Подключение..."
SignalingState.RECONNECTING -> "Переподключение..."
SignalingState.ERROR -> "Ошибка подключения"
SignalingState.DISCONNECTED -> "Отключено"
},
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center
)
Text(
text = when (sessionState) {
SessionState.WAITING -> "Ожидание звонка от оператора"
SessionState.INCOMING -> "Входящий звонок"
SessionState.ACTIVE -> "Активная сессия"
SessionState.ENDED -> "Сессия завершена"
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
}
}
@Composable
fun IncomingCallDialog(
incomingCall: IncomingCall,
onAccept: () -> Unit,
onReject: () -> Unit,
modifier: Modifier = Modifier
) {
Dialog(
onDismissRequest = { /* Не позволяем закрыть диалог без выбора */ }
) {
Card(
modifier = modifier,
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Иконка входящего звонка
Icon(
imageVector = Icons.Default.Phone,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Входящий звонок",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "От: ${incomingCall.operatorName}",
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center
)
Text(
text = "ID: ${incomingCall.operatorId}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Кнопка отклонения
OutlinedButton(
onClick = onReject,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Отклонить")
}
// Кнопка принятия
Button(
onClick = onAccept,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = Icons.Default.Call,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Принять")
}
}
}
}
}
}
@Composable
fun ActiveCallCard(
incomingCall: IncomingCall,
onEndCall: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Активный звонок",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
Text(
text = "С оператором: ${incomingCall.operatorName}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Button(
onClick = onEndCall,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Завершить")
}
}
}
}
}

View File

@@ -0,0 +1,572 @@
package com.example.godeye.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.example.godeye.models.VideoStatistics
import org.webrtc.SurfaceViewRenderer
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StreamingVideoView(
isStreaming: Boolean,
statistics: VideoStatistics?,
onStartStreaming: () -> Unit,
onStopStreaming: () -> Unit,
webRTCManager: com.example.godeye.managers.WebRTCManager? = null,
modifier: Modifier = Modifier
) {
val scrollState = rememberScrollState()
val context = LocalContext.current
// Состояния для сворачивания секций
var cameraExpanded by remember { mutableStateOf(true) }
var statsExpanded by remember { mutableStateOf(true) }
var operatorExpanded by remember { mutableStateOf(true) }
var trafficExpanded by remember { mutableStateOf(true) }
var debugExpanded by remember { mutableStateOf(false) }
// Состояние для SurfaceViewRenderer
var surfaceRenderer by remember { mutableStateOf<SurfaceViewRenderer?>(null) }
// ИСПРАВЛЕНИЕ: Улучшенная инициализация камеры для предпросмотра
LaunchedEffect(webRTCManager) {
if (webRTCManager != null && surfaceRenderer == null) {
try {
android.util.Log.d("StreamingVideoView", "🎥 Initializing camera preview renderer...")
// Создаем SurfaceViewRenderer
val renderer = SurfaceViewRenderer(context).apply {
setScalingType(org.webrtc.RendererCommon.ScalingType.SCALE_ASPECT_FIT)
setEnableHardwareScaler(true)
setMirror(false) // Отключаем зеркалирование для корректного отображения
}
// ИСПРАВЛЕНИЕ: Инициализируем с EGL контекстом от WebRTCManager
webRTCManager.getEglBaseContext()?.let { eglContext ->
renderer.init(eglContext, null)
surfaceRenderer = renderer
// КРИТИЧНО: Подключаем к WebRTCManager ПОСЛЕ инициализации
webRTCManager.attachLocalVideoRenderer(renderer)
// НОВОЕ: Запускаем предпросмотр камеры немедленно
webRTCManager.startCameraPreview()
android.util.Log.d("StreamingVideoView", "✅ Camera preview renderer initialized and started successfully")
} ?: run {
android.util.Log.e("StreamingVideoView", "❌ EGL context is null - cannot initialize camera preview")
}
} catch (e: Exception) {
android.util.Log.e("StreamingVideoView", "❌ Failed to initialize camera preview renderer", e)
}
}
}
// ИСПРАВЛЕНИЕ: Правильная очистка рендерера при уничтожении
DisposableEffect(surfaceRenderer, webRTCManager) {
onDispose {
try {
if (surfaceRenderer != null && webRTCManager != null) {
android.util.Log.d("StreamingVideoView", "🧹 Cleaning up camera preview renderer...")
// Останавливаем предпросмотр
webRTCManager.stopCameraPreview()
// Отключаем рендерер
webRTCManager.detachLocalVideoRenderer()
// Освобождаем ресурсы
surfaceRenderer?.release()
surfaceRenderer = null
android.util.Log.d("StreamingVideoView", "✅ Camera preview renderer cleaned up successfully")
}
} catch (e: Exception) {
android.util.Log.e("StreamingVideoView", "❌ Failed to cleanup camera preview renderer", e)
}
}
}
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 1. Плашка предпросмотра камеры
CollapsibleCard(
title = "📹 Предпросмотр камеры",
expanded = cameraExpanded,
onToggle = { cameraExpanded = !cameraExpanded }
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(240.dp)
.background(
Color.Black,
RoundedCornerShape(8.dp)
)
) {
// WebRTC SurfaceView для предпросмотра
AndroidView(
factory = { context ->
SurfaceViewRenderer(context).apply {
setScalingType(org.webrtc.RendererCommon.ScalingType.SCALE_ASPECT_FIT)
setEnableHardwareScaler(true)
// Инициализируем с EGL контекстом от WebRTCManager
webRTCManager?.getEglBaseContext()?.let { eglContext ->
try {
init(eglContext, null)
surfaceRenderer = this
// ИСПРАВЛЕНИЕ: Подключаем рендерер к WebRTCManager сразу
webRTCManager.attachLocalVideoRenderer(this)
android.util.Log.d("StreamingVideoView", "✅ Camera renderer attached in factory")
} catch (e: Exception) {
android.util.Log.e("StreamingVideoView", "❌ Failed to init SurfaceViewRenderer", e)
}
}
}
},
update = { view ->
// Убираем дублирование инициализации
android.util.Log.d("StreamingVideoView", "AndroidView update called")
},
modifier = Modifier.fillMaxSize()
)
// Мигающий индикатор REC
if (isStreaming) {
RecordingIndicator(
modifier = Modifier
.align(Alignment.TopStart)
.padding(12.dp)
)
}
// Кнопки управления
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = if (isStreaming) onStopStreaming else onStartStreaming,
colors = ButtonDefaults.buttonColors(
containerColor = if (isStreaming) Color.Red else MaterialTheme.colorScheme.primary
)
) {
Icon(
if (isStreaming) Icons.Default.Stop else Icons.Default.PlayArrow,
contentDescription = null
)
Spacer(modifier = Modifier.width(4.dp))
Text(if (isStreaming) "Стоп" else "Старт")
}
IconButton(
onClick = {
webRTCManager?.switchCamera()
},
modifier = Modifier.background(
Color.Black.copy(alpha = 0.5f),
RoundedCornerShape(50)
)
) {
Icon(
Icons.Default.FlipCameraAndroid,
contentDescription = "Переключить камеру",
tint = Color.White
)
}
}
}
}
// 2. Характеристики видеопотока
CollapsibleCard(
title = "📊 Характеристики видеопотока",
expanded = statsExpanded,
onToggle = { statsExpanded = !statsExpanded }
) {
statistics?.let { stats ->
VideoStatisticsContent(statistics = stats)
} ?: run {
Text(
"Статистика недоступна",
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
}
}
// 3. Подключенный оператор
CollapsibleCard(
title = "👤 Подключенный оператор",
expanded = operatorExpanded,
onToggle = { operatorExpanded = !operatorExpanded }
) {
OperatorInfoContent(isConnected = isStreaming)
}
// 4. Переданный трафик
CollapsibleCard(
title = "📡 Переданный трафик",
expanded = trafficExpanded,
onToggle = { trafficExpanded = !trafficExpanded }
) {
TrafficInfoContent(statistics = statistics)
}
// 5. Отладка и диагностика
CollapsibleCard(
title = "🔧 Отладка и диагностика",
expanded = debugExpanded,
onToggle = { debugExpanded = !debugExpanded }
) {
DebugInfoContent()
}
// Мониторинг сетевого трафика (всегда внизу)
NetworkTrafficMonitor(
sessionId = if (isStreaming) "active_session" else null,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
private fun RecordingIndicator(
modifier: Modifier = Modifier
) {
var isVisible by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
while (true) {
kotlinx.coroutines.delay(1000)
isVisible = !isVisible
}
}
if (isVisible) {
Card(
modifier = modifier,
colors = CardDefaults.cardColors(containerColor = Color.Red),
shape = RoundedCornerShape(16.dp)
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
Icons.Default.FiberManualRecord,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(8.dp)
)
Text(
"REC",
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CollapsibleCard(
title: String,
expanded: Boolean,
onToggle: () -> Unit,
content: @Composable () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column {
// Заголовок (всегда видим)
Surface(
onClick = onToggle,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
Icon(
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (expanded) "Свернуть" else "Развернуть",
tint = MaterialTheme.colorScheme.primary
)
}
}
// Содержимое (показывается только при развернутом состоянии)
if (expanded) {
Column(
modifier = Modifier.padding(16.dp)
) {
content()
}
}
}
}
}
@Composable
private fun VideoStatisticsContent(statistics: VideoStatistics) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
StatisticRow("FPS", "${statistics.fps}")
StatisticRow("Разрешение", "${statistics.width}x${statistics.height}")
StatisticRow("Битрейт", formatBitrate(statistics.bitrate))
StatisticRow("Переданные кадры", "${statistics.framerate}")
StatisticRow("Потерянные пакеты", "${statistics.packetsLost}")
StatisticRow("Задержка (RTT)", "${statistics.rtt}ms")
StatisticRow("Джиттер", "${String.format("%.2f", statistics.jitter)}ms")
}
}
@Composable
private fun OperatorInfoContent(isConnected: Boolean) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.Circle,
contentDescription = null,
tint = if (isConnected) Color.Green else Color.Gray,
modifier = Modifier.size(12.dp)
)
Text(
if (isConnected) "Подключен" else "Не подключен",
fontWeight = FontWeight.Medium,
color = if (isConnected) Color.Green else Color.Gray
)
}
if (isConnected) {
StatisticRow("ID оператора", "OP_12345")
StatisticRow("Время сессии", "05:23")
StatisticRow("Качество связи", "Отлично")
StatisticRow("Тип камеры", "Основная")
}
}
}
@Composable
private fun TrafficInfoContent(statistics: VideoStatistics?) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
statistics?.let { stats ->
StatisticRow("Всего отправлено", formatBytes(stats.bytesSent))
StatisticRow("Скорость", formatBitrate(stats.bitrate))
StatisticRow("Сжатие", "H.264")
StatisticRow("Использовано трафика", formatBytes(stats.bytesSent))
} ?: run {
Text(
"Нет данных о трафике",
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
}
}
}
@Composable
private fun DebugInfoContent() {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
// Информация о соединении
Text(
"Информация о соединении",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
)
StatisticRow("WebRTC состояние", "CONNECTED")
StatisticRow("ICE состояние", "COMPLETED")
StatisticRow("Signaling", "STABLE")
HorizontalDivider()
// Сетевая информация
Text(
"Сетевая информация",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
)
StatisticRow("Локальный IP", "192.168.1.105")
StatisticRow("Внешний IP", "203.45.67.89")
StatisticRow("STUN сервер", "stun.l.google.com:19302")
StatisticRow("Порт", "51234")
HorizontalDivider()
// Кнопки действий
Text(
"Действия отладки",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedButton(
onClick = { /* Тест соединения */ },
modifier = Modifier.weight(1f)
) {
Text("Тест связи", fontSize = 12.sp)
}
OutlinedButton(
onClick = { /* Перезапуск */ },
modifier = Modifier.weight(1f)
) {
Text("Перезапуск", fontSize = 12.sp)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedButton(
onClick = { /* Логи */ },
modifier = Modifier.weight(1f)
) {
Text("Экспорт логов", fontSize = 12.sp)
}
OutlinedButton(
onClick = { /* Диагностика */ },
modifier = Modifier.weight(1f)
) {
Text("Диагностика", fontSize = 12.sp)
}
}
// Переключатели отладки
HorizontalDivider()
Text(
"Настройки отладки",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
)
var verboseLogging by remember { mutableStateOf(false) }
var showStatistics by remember { mutableStateOf(true) }
var autoReconnect by remember { mutableStateOf(true) }
DebugSwitchRow(
"Детальное логирование",
verboseLogging
) { verboseLogging = it }
DebugSwitchRow(
"Показывать статистику",
showStatistics
) { showStatistics = it }
DebugSwitchRow(
"Автоперезподключение",
autoReconnect
) { autoReconnect = it }
}
}
@Composable
private fun DebugSwitchRow(
label: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
label,
fontSize = 14.sp,
modifier = Modifier.weight(1f)
)
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = Modifier.height(24.dp)
)
}
}
@Composable
private fun StatisticRow(
label: String,
value: String
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
modifier = Modifier.weight(1f)
)
Text(
text = value,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
}
}
private fun formatBytes(bytes: Long): String {
return when {
bytes < 1024 -> "$bytes B"
bytes < 1024 * 1024 -> "${bytes / 1024} KB"
bytes < 1024 * 1024 * 1024 -> "${bytes / (1024 * 1024)} MB"
else -> "${bytes / (1024 * 1024 * 1024)} GB"
}
}
private fun formatBitrate(bitrate: Long): String {
return when {
bitrate < 1000 -> "${bitrate} bps"
bitrate < 1000000 -> "${bitrate / 1000} Kbps"
else -> "${bitrate / 1000000} Mbps"
}
}

View File

@@ -0,0 +1,850 @@
package com.example.godeye.ui.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.godeye.managers.AutoApprovalManager
import com.example.godeye.managers.ConnectionManager
import com.example.godeye.utils.PreferenceManager
import com.example.godeye.utils.PermissionHelper
import com.example.godeye.ui.components.StreamingVideoView
import com.example.godeye.models.StreamingState
import com.example.godeye.managers.WebRTCManager
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
connectionManager: ConnectionManager,
autoApprovalManager: AutoApprovalManager,
preferenceManager: PreferenceManager,
permissionHelper: PermissionHelper,
onOpenSettings: () -> Unit
) {
val connectionState by connectionManager.connectionState.collectAsState(initial = ConnectionManager.ConnectionState.DISCONNECTED)
val deviceInfo by connectionManager.deviceInfo.collectAsState(initial = null)
val activeSessions by autoApprovalManager.activeSessions.collectAsState(initial = emptyList())
val pendingRequest by autoApprovalManager.pendingRequest.collectAsState(initial = null)
val serverUrl by preferenceManager.serverUrl.collectAsState(initial = "")
val deviceName by preferenceManager.deviceName.collectAsState(initial = "")
val autoConnect by preferenceManager.autoConnect.collectAsState(initial = false)
val autoApprove by preferenceManager.autoApprove.collectAsState(initial = false)
// НОВОЕ: Состояние трансляции
val streamingState by connectionManager.webRTCManager.streamingState.collectAsState()
val isStreamingMode = streamingState.isStreaming
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFF0A1828),
Color(0xFF1E3A5F),
Color(0xFF2D4A73)
)
)
)
) {
if (isStreamingMode) {
// НОВОЕ: Режим трансляции с видео и статистикой
StreamingModeContent(
streamingState = streamingState,
onSwitchCamera = { connectionManager.webRTCManager.switchCamera() },
onStopStreaming = {
streamingState.sessionId?.let { sessionId ->
connectionManager.webRTCManager.stopStreaming(sessionId)
}
},
webRTCManager = connectionManager.webRTCManager // Передаем WebRTCManager
)
} else {
// Обычный режим с плашками настроек
NormalModeContent(
connectionState = connectionState,
deviceInfo = deviceInfo,
activeSessions = activeSessions,
pendingRequest = pendingRequest,
serverUrl = serverUrl,
deviceName = deviceName,
autoConnect = autoConnect,
autoApprove = autoApprove,
connectionManager = connectionManager,
autoApprovalManager = autoApprovalManager,
preferenceManager = preferenceManager,
permissionHelper = permissionHelper,
onOpenSettings = onOpenSettings
)
}
}
}
@Composable
private fun StreamingModeContent(
streamingState: StreamingState,
onSwitchCamera: () -> Unit,
onStopStreaming: () -> Unit,
webRTCManager: com.example.godeye.managers.WebRTCManager // Добавлен параметр webRTCManager
) {
StreamingVideoView(
isStreaming = streamingState.isStreaming,
statistics = streamingState.statistics,
onStartStreaming = { /* Здесь можно добавить логику запуска трансляции */ },
onStopStreaming = onStopStreaming,
webRTCManager = webRTCManager,
modifier = Modifier.fillMaxSize()
)
}
@Composable
private fun NormalModeContent(
connectionState: ConnectionManager.ConnectionState,
deviceInfo: Any?,
activeSessions: List<Any>,
pendingRequest: Any?,
serverUrl: String,
deviceName: String,
autoConnect: Boolean,
autoApprove: Boolean,
connectionManager: ConnectionManager,
autoApprovalManager: AutoApprovalManager,
preferenceManager: PreferenceManager,
permissionHelper: PermissionHelper,
onOpenSettings: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Заголовок с настройками
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "GodEye",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
color = Color(0xFF00BFFF)
)
Text(
text = "Signal Center",
style = MaterialTheme.typography.titleMedium,
color = Color(0xFF40E0D0)
)
}
IconButton(
onClick = onOpenSettings,
modifier = Modifier
.background(
Color(0xFF1A2B3D),
CircleShape
)
) {
Icon(
Icons.Default.Settings,
contentDescription = "Настройки",
tint = Color(0xFF00BFFF)
)
}
}
// Карточка статуса подключения
ConnectionStatusCard(
connectionState = connectionState,
serverUrl = serverUrl,
deviceName = deviceName,
deviceInfo = deviceInfo as? com.example.godeye.models.DeviceInfo,
onConnect = { connectionManager.connect() },
onDisconnect = { connectionManager.disconnect() }
)
// Карточка настроек
SettingsOverviewCard(
autoConnect = autoConnect,
autoApprove = autoApprove,
hasAllPermissions = permissionHelper.hasAllPermissions(),
onOpenSettings = onOpenSettings
)
// Тестовая кнопка для проверки автоподтверждения
if (connectionState == ConnectionManager.ConnectionState.CONNECTED) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2D4A22).copy(alpha = 0.8f)
),
border = BorderStroke(1.dp, Color(0xFF4CAF50))
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "Тестирование автоподтверждения",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF4CAF50)
)
Text(
text = "Имитирует запрос камеры от оператора",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFFB3B3B3)
)
}
Button(
onClick = { connectionManager.testAutoApproval() },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF4CAF50)
)
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Тест"
)
Spacer(modifier = Modifier.width(8.dp))
Text("Тест")
}
}
}
}
// Pending запрос (если есть)
pendingRequest?.let { request ->
if (request is org.json.JSONObject) {
PendingRequestCard(
request = request,
onApprove = { sessionId, operatorId, cameraType ->
autoApprovalManager.approveRequest(sessionId, operatorId, cameraType)
},
onDeny = { sessionId ->
autoApprovalManager.denyRequest(sessionId)
}
)
}
}
// Активные сессии
if (activeSessions.isNotEmpty()) {
Text(
text = "Активные сессии (${activeSessions.size})",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF00BFFF)
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(activeSessions) { session ->
if (session is AutoApprovalManager.CameraSession) {
ActiveSessionCard(session = session)
}
}
}
} else {
// Пустое состояние
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1A2B3D).copy(alpha = 0.8f)
),
border = BorderStroke(1.dp, Color(0xFF40E0D0).copy(alpha = 0.3f))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = Color(0xFF40E0D0)
)
Text(
text = "Нет активных сессий",
style = MaterialTheme.typography.titleMedium,
color = Color(0xFFE0F4FF)
)
Text(
text = "Ожидание запросов от операторов",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFFB0C4DE)
)
}
}
}
}
}
@Composable
private fun ConnectionStatusCard(
connectionState: ConnectionManager.ConnectionState,
serverUrl: String,
deviceName: String,
deviceInfo: com.example.godeye.models.DeviceInfo?,
onConnect: () -> Unit,
onDisconnect: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = when (connectionState) {
ConnectionManager.ConnectionState.CONNECTED -> MaterialTheme.colorScheme.primaryContainer
ConnectionManager.ConnectionState.CONNECTING -> MaterialTheme.colorScheme.tertiaryContainer
ConnectionManager.ConnectionState.ERROR -> MaterialTheme.colorScheme.errorContainer
ConnectionManager.ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.surfaceVariant
ConnectionManager.ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.tertiaryContainer
}
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Подключение к серверу",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
when (connectionState) {
ConnectionManager.ConnectionState.CONNECTED -> {
Button(
onClick = onDisconnect,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("Отключиться")
}
}
ConnectionManager.ConnectionState.CONNECTING -> {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
}
else -> {
Button(onClick = onConnect) {
Text("Подключиться")
}
}
}
}
// Статус
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
when (connectionState) {
ConnectionManager.ConnectionState.CONNECTED -> Icons.Default.CheckCircle
ConnectionManager.ConnectionState.CONNECTING -> Icons.Default.Refresh
ConnectionManager.ConnectionState.ERROR -> Icons.Default.Warning
ConnectionManager.ConnectionState.DISCONNECTED -> Icons.Default.Close
ConnectionManager.ConnectionState.RECONNECTING -> Icons.Default.Refresh
},
contentDescription = null,
tint = when (connectionState) {
ConnectionManager.ConnectionState.CONNECTED -> MaterialTheme.colorScheme.onPrimaryContainer
ConnectionManager.ConnectionState.CONNECTING -> MaterialTheme.colorScheme.onTertiaryContainer
ConnectionManager.ConnectionState.ERROR -> MaterialTheme.colorScheme.onErrorContainer
ConnectionManager.ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.onSurfaceVariant
ConnectionManager.ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.onTertiaryContainer
}
)
Text(
text = when (connectionState) {
ConnectionManager.ConnectionState.CONNECTED -> "Подключено"
ConnectionManager.ConnectionState.CONNECTING -> "Подключение..."
ConnectionManager.ConnectionState.ERROR -> "Ошибка соединения"
ConnectionManager.ConnectionState.DISCONNECTED -> "Отключено"
ConnectionManager.ConnectionState.RECONNECTING -> "Переподключение..."
},
style = MaterialTheme.typography.bodyMedium
)
}
// Информация о сервере
Text(
text = "Сервер: $serverUrl",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "Устройство: $deviceName",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (deviceInfo is com.example.godeye.models.DeviceInfo) {
Text(
text = "Модель: ${deviceInfo.manufacturer} ${deviceInfo.model} (Android ${deviceInfo.androidVersion})",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
private fun SettingsOverviewCard(
autoConnect: Boolean,
autoApprove: Boolean,
hasAllPermissions: Boolean,
onOpenSettings: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Быстрые настройки",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
TextButton(onClick = onOpenSettings) {
Text("Все настройки")
Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null)
}
}
// Статусы настроек
SettingStatusRow("Автоподключение", autoConnect)
SettingStatusRow("Автоподтверждение", autoApprove, isWarning = autoApprove)
SettingStatusRow("Разрешения", hasAllPermissions, isError = !hasAllPermissions)
}
}
}
@Composable
private fun SettingStatusRow(
title: String,
enabled: Boolean,
isWarning: Boolean = false,
isError: Boolean = false
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
if (enabled) Icons.Default.Check else Icons.Default.Close,
contentDescription = null,
tint = when {
isError -> MaterialTheme.colorScheme.error
isWarning -> MaterialTheme.colorScheme.tertiary
enabled -> MaterialTheme.colorScheme.primary
else -> MaterialTheme.colorScheme.onSurfaceVariant
}
)
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = when {
isError -> MaterialTheme.colorScheme.error
isWarning -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
}
@Composable
private fun PendingRequestCard(
request: org.json.JSONObject,
onApprove: (String, String, String) -> Unit,
onDeny: (String) -> Unit
) {
val sessionId = request.optString("sessionId")
val operatorId = request.optString("operatorId")
val cameraType = request.optString("cameraType", "back")
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2D4A22).copy(alpha = 0.8f)
),
border = BorderStroke(1.dp, Color(0xFFFF9800))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.NotificationImportant,
contentDescription = null,
tint = Color(0xFFFF9800),
modifier = Modifier.size(24.dp)
)
Text(
text = "Входящий запрос камеры",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color(0xFFFF9800)
)
}
HorizontalDivider(
color = Color(0xFFFF9800).copy(alpha = 0.3f),
thickness = 1.dp
)
// UUID оператора
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "UUID оператора:",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFB0C4DE)
)
Text(
text = operatorId,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = Color(0xFFE0F4FF),
modifier = Modifier
.background(
Color(0xFF2D4A73).copy(alpha = 0.5f),
shape = MaterialTheme.shapes.small
)
.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
// Информация о запросе
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(
text = "Запрашиваемая камера:",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFB0C4DE)
)
Text(
text = getCameraDisplayName(cameraType),
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFFE0F4FF)
)
}
Column(
horizontalAlignment = Alignment.End
) {
Text(
text = "ID сессии:",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFB0C4DE)
)
Text(
text = sessionId.take(8) + "...",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFE0F4FF)
)
}
}
}
// Предупреждение
Card(
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFF9800).copy(alpha = 0.1f)
),
border = BorderStroke(1.dp, Color(0xFFFF9800).copy(alpha = 0.3f))
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Warning,
contentDescription = null,
tint = Color(0xFFFF9800),
modifier = Modifier.size(16.dp)
)
Text(
text = "Убедитесь, что вы доверяете этому оператору",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFFF9800)
)
}
}
// Кнопки действий
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedButton(
onClick = { onDeny(sessionId) },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = Color(0xFFF44336)
),
border = BorderStroke(1.dp, Color(0xFFF44336))
) {
Icon(
Icons.Default.Close,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Отклонить")
}
Button(
onClick = { onApprove(sessionId, operatorId, cameraType) },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF4CAF50)
)
) {
Icon(
Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Разрешить")
}
}
}
}
}
@Composable
private fun ActiveSessionCard(
session: AutoApprovalManager.CameraSession
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1A2B3D).copy(alpha = 0.9f)
),
border = BorderStroke(1.dp, Color(0xFF00BFFF).copy(alpha = 0.5f))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Заголовок с состоянием
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Подключенный оператор",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color(0xFF00BFFF)
)
// Индикатор состояния
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Box(
modifier = Modifier
.size(8.dp)
.background(
color = when (session.streamingState) {
AutoApprovalManager.StreamingState.STREAMING -> Color(0xFF4CAF50)
AutoApprovalManager.StreamingState.CONNECTING -> Color(0xFFFF9800)
AutoApprovalManager.StreamingState.ERROR -> Color(0xFFF44336)
else -> Color(0xFF9E9E9E)
},
shape = CircleShape
)
)
Text(
text = when (session.streamingState) {
AutoApprovalManager.StreamingState.STREAMING -> "Активна"
AutoApprovalManager.StreamingState.CONNECTING -> "Подключение"
AutoApprovalManager.StreamingState.ERROR -> "Ошибка"
else -> "Ожидание"
},
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFB0C4DE)
)
}
}
HorizontalDivider(
color = Color(0xFF40E0D0).copy(alpha = 0.3f),
thickness = 1.dp
)
// UUID оператора
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "UUID оператора:",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFB0C4DE)
)
Text(
text = session.operatorId,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = Color(0xFFE0F4FF),
// Делаем текст выделяемым для копирования
modifier = Modifier
.background(
Color(0xFF2D4A73).copy(alpha = 0.5f),
shape = MaterialTheme.shapes.small
)
.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
// Информация о камере и времени
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
text = "Камера:",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFB0C4DE)
)
Text(
text = getCameraDisplayName(session.cameraType),
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFE0F4FF)
)
}
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.End
) {
Text(
text = "Длительность:",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFB0C4DE)
)
Text(
text = formatDuration(System.currentTimeMillis() - session.startTime),
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFE0F4FF)
)
}
}
// Дополнительная информация
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (session.isAutoApproved) "Автоматически одобрено" else "Одобрено вручную",
style = MaterialTheme.typography.bodySmall,
color = if (session.isAutoApproved) Color(0xFF4CAF50) else Color(0xFF2196F3)
)
Icon(
imageVector = Icons.Default.Videocam,
contentDescription = "Камера активна",
tint = Color(0xFF00BFFF),
modifier = Modifier.size(20.dp)
)
}
}
}
}
// Вспомогательная функция для форматирования времени
private fun formatDuration(durationMs: Long): String {
val seconds = (durationMs / 1000) % 60
val minutes = (durationMs / (1000 * 60)) % 60
val hours = (durationMs / (1000 * 60 * 60)) % 24
return when {
hours > 0 -> String.format("%02d:%02d:%02d", hours, minutes, seconds)
else -> String.format("%02d:%02d", minutes, seconds)
}
}
// Вспомогательная функция для отображения типа камеры
private fun getCameraDisplayName(cameraType: String): String {
return when (cameraType) {
"back" -> "Основная камера"
"front" -> "Фронтальная камера"
"wide" -> "Широкоугольная камера"
"telephoto" -> "Телеобъектив"
else -> "Камера"
}
}

View File

@@ -0,0 +1,350 @@
package com.example.godeye.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.godeye.utils.PreferenceManager
import com.example.godeye.utils.PermissionHelper
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
preferenceManager: PreferenceManager,
permissionHelper: PermissionHelper,
onNavigateBack: () -> Unit
) {
val serverUrl by preferenceManager.serverUrl.collectAsStateWithLifecycle()
val deviceName by preferenceManager.deviceName.collectAsStateWithLifecycle()
val autoConnect by preferenceManager.autoConnect.collectAsStateWithLifecycle()
val autoApprove by preferenceManager.autoApprove.collectAsStateWithLifecycle()
var tempServerUrl by remember(serverUrl) { mutableStateOf(serverUrl) }
var tempDeviceName by remember(deviceName) { mutableStateOf(deviceName) }
var showPermissionDialog by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Заголовок
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Настройки",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.Close, contentDescription = "Закрыть")
}
}
HorizontalDivider()
// Секция "Подключение к серверу"
SettingsSection(title = "Подключение к серверу") {
// URL сервера
OutlinedTextField(
value = tempServerUrl,
onValueChange = { tempServerUrl = it },
label = { Text("URL сервера") },
placeholder = { Text("http://192.168.219.108:3001") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
// Кнопка сохранения URL
Button(
onClick = {
if (tempServerUrl.isNotBlank()) {
preferenceManager.setServerUrl(tempServerUrl.trim())
}
},
modifier = Modifier.align(Alignment.End),
enabled = tempServerUrl.trim() != serverUrl
) {
Icon(Icons.Default.Done, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Сохранить URL")
}
// Автоматическое подключение
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Автоматическое подключение",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
Text(
text = "Подключаться к серверу при запуске приложения",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = autoConnect,
onCheckedChange = { preferenceManager.setAutoConnect(it) }
)
}
}
}
// Секция "Устройство"
SettingsSection(title = "Устройство") {
// Имя устройства
OutlinedTextField(
value = tempDeviceName,
onValueChange = { tempDeviceName = it },
label = { Text("Имя устройства") },
placeholder = { Text("Android Device") },
leadingIcon = { Icon(Icons.Default.Phone, contentDescription = null) },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
// Кнопка сохранения имени
Button(
onClick = {
if (tempDeviceName.isNotBlank()) {
preferenceManager.setDeviceName(tempDeviceName.trim())
}
},
modifier = Modifier.align(Alignment.End),
enabled = tempDeviceName.trim() != deviceName
) {
Icon(Icons.Default.Done, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Сохранить имя")
}
// ID устройства (только для чтения)
OutlinedTextField(
value = preferenceManager.getDeviceId(),
onValueChange = { },
label = { Text("ID устройства") },
leadingIcon = { Icon(Icons.Default.Info, contentDescription = null) },
modifier = Modifier.fillMaxWidth(),
enabled = false,
singleLine = true
)
}
// Секция "Безопасность"
SettingsSection(title = "Безопасность") {
// Автоматическое подтверждение
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (autoApprove)
MaterialTheme.colorScheme.errorContainer
else
MaterialTheme.colorScheme.surfaceVariant
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Автоматическое подтверждение",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = if (autoApprove)
MaterialTheme.colorScheme.onErrorContainer
else
MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = if (autoApprove) "⚠️ Операторы получат доступ без подтверждения"
else "Запрашивать подтверждение для каждого запроса",
style = MaterialTheme.typography.bodyMedium,
color = if (autoApprove)
MaterialTheme.colorScheme.onErrorContainer
else
MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = autoApprove,
onCheckedChange = { preferenceManager.setAutoApprove(it) }
)
}
}
}
// Секция "Разрешения"
SettingsSection(title = "Разрешения") {
val hasAllPermissions = permissionHelper.hasAllPermissions()
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (hasAllPermissions)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.errorContainer
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
if (hasAllPermissions) Icons.Default.CheckCircle else Icons.Default.Warning,
contentDescription = null,
tint = if (hasAllPermissions)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onErrorContainer
)
Text(
text = if (hasAllPermissions) "Все разрешения предоставлены" else "Требуются разрешения",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = if (hasAllPermissions)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onErrorContainer
)
}
// Список разрешений
PermissionHelper.REQUIRED_PERMISSIONS.forEach { permission ->
val hasPermission = permissionHelper.hasPermission(permission)
val permissionName = getPermissionDisplayName(permission)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
if (hasPermission) Icons.Default.Check else Icons.Default.Close,
contentDescription = null,
tint = if (hasPermission)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onErrorContainer
)
Text(
text = permissionName,
style = MaterialTheme.typography.bodyMedium,
color = if (hasAllPermissions)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onErrorContainer
)
}
}
if (!hasAllPermissions) {
Button(
onClick = { showPermissionDialog = true },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Lock, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Запросить разрешения")
}
}
}
}
}
}
// Диалог запроса разрешений
if (showPermissionDialog) {
AlertDialog(
onDismissRequest = { showPermissionDialog = false },
title = { Text("Разрешения приложения") },
text = {
Text("Для корректной работы приложению необходимы разрешения на доступ к камере, микрофону и интернету.")
},
confirmButton = {
TextButton(
onClick = {
showPermissionDialog = false
permissionHelper.requestPermissions { granted ->
// Результат обработается автоматически через recomposition
}
}
) {
Text("Предоставить")
}
},
dismissButton = {
TextButton(onClick = { showPermissionDialog = false }) {
Text("Отмена")
}
}
)
}
}
@Composable
private fun SettingsSection(
title: String,
content: @Composable ColumnScope.() -> Unit
) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
content()
}
}
private fun getPermissionDisplayName(permission: String): String {
return when (permission) {
android.Manifest.permission.CAMERA -> "Камера"
android.Manifest.permission.RECORD_AUDIO -> "Микрофон"
android.Manifest.permission.INTERNET -> "Интернет"
android.Manifest.permission.ACCESS_NETWORK_STATE -> "Состояние сети"
android.Manifest.permission.WAKE_LOCK -> "Предотвращение блокировки"
android.Manifest.permission.POST_NOTIFICATIONS -> "Уведомления"
else -> permission.split(".").lastOrNull() ?: permission
}
}

View File

@@ -5,93 +5,54 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/**
* Цветовая палитра GodEye согласно ТЗ
*/
// Цветовая схема GodEye
object GodEyeColors {
// Основные цвета приложения
val BlackPure = Color(0xFF000000)
val BlackSoft = Color(0xFF1A1A1A)
val BlackMedium = Color(0xFF2D2D2D)
val Primary = Color(0xFF1976D2)
val PrimaryVariant = Color(0xFF1565C0)
val Secondary = Color(0xFF03DAC6)
val Background = Color(0xFF121212)
val Surface = Color(0xFF1E1E1E)
val Error = Color(0xFFCF6679)
val OnPrimary = Color.White
val OnSecondary = Color.Black
val OnBackground = Color.White
val OnSurface = Color.White
val OnError = Color.Black
val IvoryPure = Color(0xFFFFFFF0)
val IvorySoft = Color(0xFFF5F5DC)
val IvoryMedium = Color(0xFFE6E6D4)
val NavyDark = Color(0xFF0F1419)
val NavyMedium = Color(0xFF1E2328)
val NavyLight = Color(0xFF2D3748)
// Функциональные цвета
val RecordRed = Color(0xFFFF3B30)
val WarningAmber = Color(0xFFFF9500)
val SuccessGreen = Color(0xFF30D158)
val InfoBlue = Color(0xFF007AFF)
// Градиенты
val PrimaryGradientStart = NavyDark
val PrimaryGradientEnd = BlackSoft
val AccentGradientStart = NavyLight
val AccentGradientEnd = NavyMedium
// Статусные цвета
val Connected = Color(0xFF4CAF50)
val Disconnected = Color(0xFFF44336)
val Waiting = Color(0xFFFF9800)
}
private val DarkColorScheme = darkColorScheme(
primary = GodEyeColors.NavyLight,
onPrimary = GodEyeColors.IvoryPure,
primaryContainer = GodEyeColors.NavyMedium,
onPrimaryContainer = GodEyeColors.IvorySoft,
secondary = GodEyeColors.IvoryMedium,
onSecondary = GodEyeColors.BlackPure,
secondaryContainer = GodEyeColors.BlackMedium,
onSecondaryContainer = GodEyeColors.IvoryPure,
tertiary = GodEyeColors.WarningAmber,
onTertiary = GodEyeColors.BlackPure,
error = GodEyeColors.RecordRed,
onError = GodEyeColors.IvoryPure,
background = GodEyeColors.BlackPure,
onBackground = GodEyeColors.IvoryPure,
surface = GodEyeColors.BlackSoft,
onSurface = GodEyeColors.IvoryPure,
surfaceVariant = GodEyeColors.BlackMedium,
onSurfaceVariant = GodEyeColors.IvorySoft,
outline = GodEyeColors.NavyMedium,
outlineVariant = GodEyeColors.NavyLight
primary = GodEyeColors.Primary,
secondary = GodEyeColors.Secondary,
tertiary = GodEyeColors.PrimaryVariant,
background = GodEyeColors.Background,
surface = GodEyeColors.Surface,
error = GodEyeColors.Error,
onPrimary = GodEyeColors.OnPrimary,
onSecondary = GodEyeColors.OnSecondary,
onTertiary = GodEyeColors.OnPrimary,
onBackground = GodEyeColors.OnBackground,
onSurface = GodEyeColors.OnSurface,
onError = GodEyeColors.OnError,
)
private val LightColorScheme = lightColorScheme(
primary = GodEyeColors.NavyMedium,
onPrimary = GodEyeColors.IvoryPure,
primaryContainer = GodEyeColors.NavyLight,
onPrimaryContainer = GodEyeColors.BlackPure,
secondary = GodEyeColors.BlackMedium,
onSecondary = GodEyeColors.IvoryPure,
secondaryContainer = GodEyeColors.IvoryMedium,
onSecondaryContainer = GodEyeColors.BlackPure,
tertiary = GodEyeColors.WarningAmber,
onTertiary = GodEyeColors.IvoryPure,
error = GodEyeColors.RecordRed,
onError = GodEyeColors.IvoryPure,
background = GodEyeColors.IvoryPure,
onBackground = GodEyeColors.BlackPure,
surface = GodEyeColors.IvorySoft,
onSurface = GodEyeColors.BlackPure,
surfaceVariant = GodEyeColors.IvoryMedium,
onSurfaceVariant = GodEyeColors.BlackMedium,
outline = GodEyeColors.NavyLight,
outlineVariant = GodEyeColors.NavyMedium
primary = GodEyeColors.Primary,
secondary = GodEyeColors.Secondary,
tertiary = GodEyeColors.PrimaryVariant,
background = Color.White,
surface = Color(0xFFF5F5F5),
error = Color(0xFFD32F2F),
onPrimary = Color.White,
onSecondary = Color.Black,
onTertiary = Color.White,
onBackground = Color.Black,
onSurface = Color.Black,
onError = Color.White,
)
@Composable
@@ -99,9 +60,10 @@ fun GodEyeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
darkTheme -> DarkColorScheme
else -> LightColorScheme
val colorScheme = if (darkTheme) {
DarkColorScheme
} else {
LightColorScheme
}
MaterialTheme(

View File

@@ -1,5 +1,7 @@
package com.example.godeye.utils
import android.content.Context
/**
* Constants - константы приложения согласно ТЗ
*/
@@ -8,7 +10,9 @@ object Constants {
// Настройки сервера согласно ТЗ
const val DEFAULT_SERVER_URL = "http://192.168.219.108:3001"
const val LOCALHOST_SERVER_URL = "http://localhost:3001"
const val LOCAL_NETWORK_SERVER_URL = "http://192.168.1.100:3001"
const val LOCAL_NETWORK_SERVER_URL = "http://192.168.219.108:3001"
const val WEBSOCKET_SERVER_URL = "ws://192.168.219.108:3000"
const val SIGNALING_SERVER_URL = "ws://192.168.219.108:8765"
// Настройки Socket.IO
const val SOCKET_TIMEOUT = 10000L
@@ -53,6 +57,11 @@ object Constants {
const val WEBRTC_OFFER_RECEIVED = "webrtc:offer"
const val WEBRTC_ANSWER_RECEIVED = "webrtc:answer"
const val WEBRTC_ICE_RECEIVED = "webrtc:ice-candidate"
const val CONNECT = "connect"
const val DISCONNECT = "disconnect"
const val DEVICE_REGISTER = "device-register"
const val KEEP_ALIVE = "keep-alive"
}
// SharedPreferences ключи согласно ТЗ
@@ -88,4 +97,37 @@ object Constants {
const val FPS_30 = 30
const val FPS_60 = 60
}
// НОВОЕ: Добавляем автоматическое определение IP сервера
fun getServerUrlForCurrentNetwork(context: Context): String {
val localIP = context.getLocalIpAddress()
// Проверяем, находимся ли мы в подсети 192.168.219.x
return if (localIP.startsWith("192.168.219.")) {
"http://192.168.219.108:3001" // Ваш сервер
} else {
// Fallback для других сетей
val serverIP = localIP.substringBeforeLast(".") + ".108"
"http://$serverIP:3001"
}
}
fun getWebSocketUrlForCurrentNetwork(context: Context): String {
val localIP = context.getLocalIpAddress()
return if (localIP.startsWith("192.168.219.")) {
"ws://192.168.219.108:3000" // Ваш сервер
} else {
val serverIP = localIP.substringBeforeLast(".") + ".108"
"ws://$serverIP:3000"
}
}
fun getSignalingUrlForCurrentNetwork(context: Context): String {
val localIP = context.getLocalIpAddress()
return if (localIP.startsWith("192.168.219.")) {
"ws://192.168.219.108:8765" // Ваш сервер
} else {
val serverIP = localIP.substringBeforeLast(".") + ".108"
"ws://$serverIP:8765"
}
}
}

View File

@@ -1,146 +1,14 @@
package com.example.godeye.utils
import android.content.Context
import android.widget.Toast
import androidx.compose.material3.SnackbarHostState
import com.example.godeye.models.AppError
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* ErrorHandler - обработка ошибок согласно ТЗ
* Централизованная обработка всех типов ошибок приложения
*/
class ErrorHandler {
/**
* Обработка ошибок приложения согласно ТЗ
*/
fun handleError(error: AppError, context: Context, scope: CoroutineScope? = null, snackbarHost: SnackbarHostState? = null) {
Logger.error("APP_ERROR", "Handling application error: ${error::class.simpleName}", null)
when (error) {
is AppError.NetworkError -> {
showNetworkError(context, scope, snackbarHost)
}
is AppError.CameraPermissionDenied -> {
showPermissionError(context, scope, snackbarHost)
}
is AppError.CameraNotAvailable -> {
showCameraError(context, scope, snackbarHost)
}
is AppError.WebRTCConnectionFailed -> {
showWebRTCError(context, scope, snackbarHost)
}
is AppError.SocketError -> {
showSocketError(context, error.message, scope, snackbarHost)
}
is AppError.UnknownError -> {
showUnknownError(context, error.throwable, scope, snackbarHost)
}
}
fun handle(error: Throwable) {
// Простая обработка ошибок: логирование
println("Error: ${error.message}")
}
/**
* Специальная обработка исключений для предотвращения крашей
*/
fun handleUncaughtException(thread: Thread, throwable: Throwable) {
Logger.error("UNCAUGHT_EXCEPTION", "Uncaught exception in thread: ${thread.name}", throwable)
// Специальная обработка известных Compose ошибок
when {
throwable.message?.contains("ACTION_HOVER_EXIT event was not cleared") == true -> {
Logger.d("Ignoring Compose hover event bug")
// Игнорируем эту ошибку, так как это известный баг Compose
return
}
throwable.message?.contains("Thread starting during runtime shutdown") == true -> {
Logger.d("Ignoring shutdown thread creation error")
// Игнорируем ошибки создания потоков при завершении
return
}
else -> {
// Для остальных ошибок делаем стандартную обработку
Logger.error("CRITICAL_ERROR", "Critical error occurred", throwable)
}
}
}
private fun showNetworkError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
val message = "Ошибка сети. Проверьте подключение к интернету."
Logger.step("ERROR_NETWORK", message)
if (scope != null && snackbarHost != null) {
scope.launch {
snackbarHost.showSnackbar(message)
}
} else {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
private fun showPermissionError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
val message = "Необходимы разрешения для работы с камерой и микрофоном."
Logger.step("ERROR_PERMISSION", message)
if (scope != null && snackbarHost != null) {
scope.launch {
snackbarHost.showSnackbar(message)
}
} else {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
private fun showCameraError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
val message = "Камера недоступна. Проверьте, что другие приложения не используют камеру."
Logger.step("ERROR_CAMERA", message)
if (scope != null && snackbarHost != null) {
scope.launch {
snackbarHost.showSnackbar(message)
}
} else {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
private fun showWebRTCError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
val message = "Ошибка WebRTC соединения. Попробуйте переподключиться."
Logger.step("ERROR_WEBRTC", message)
if (scope != null && snackbarHost != null) {
scope.launch {
snackbarHost.showSnackbar(message)
}
} else {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
private fun showSocketError(context: Context, errorMessage: String, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
val message = "Ошибка подключения к серверу: $errorMessage"
Logger.step("ERROR_SOCKET", message)
if (scope != null && snackbarHost != null) {
scope.launch {
snackbarHost.showSnackbar(message)
}
} else {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
private fun showUnknownError(context: Context, throwable: Throwable, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
val message = "Неизвестная ошибка: ${throwable.message ?: "Unknown"}"
Logger.error("ERROR_UNKNOWN", message, throwable)
if (scope != null && snackbarHost != null) {
scope.launch {
snackbarHost.showSnackbar(message)
}
} else {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
fun handleUncaughtException(thread: Thread, exception: Throwable) {
// Обработка неперехваченных исключений
println("Uncaught exception in thread ${thread.name}: ${exception.message}")
exception.printStackTrace()
}
}

View File

@@ -2,7 +2,13 @@ package com.example.godeye.utils
import android.content.Context
import android.content.SharedPreferences
import android.net.wifi.WifiManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import java.net.NetworkInterface
import java.net.InetAddress
import java.util.*
import java.text.SimpleDateFormat
fun Context.getPreferences(): SharedPreferences {
return getSharedPreferences("godeye_prefs", Context.MODE_PRIVATE)
@@ -11,3 +17,142 @@ fun Context.getPreferences(): SharedPreferences {
fun generateDeviceId(): String {
return "android_${UUID.randomUUID().toString().take(8)}"
}
/**
* НОВОЕ: Получение реального IP-адреса устройства с обновленными API
*/
fun Context.getLocalIpAddress(): String {
try {
// Метод 1: Через WiFi Manager (для WiFi соединений) - обновленный API
val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
wifiManager?.let { wifi ->
if (wifi.isWifiEnabled) {
// ИСПРАВЛЕНИЕ: Используем современный API для получения WiFi информации
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
// Для Android 10+ используем ConnectivityManager
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
connectivityManager?.let { cm ->
val activeNetwork = cm.activeNetwork
val linkProperties = cm.getLinkProperties(activeNetwork)
linkProperties?.linkAddresses?.forEach { linkAddress ->
val address = linkAddress.address
if (address is java.net.Inet4Address && !address.isLoopbackAddress) {
val ip = address.hostAddress
if (ip != null && !ip.startsWith("127.")) {
Logger.step("IP_DETECTION", "Modern API IP detected: $ip")
return ip
}
}
}
}
} else {
// Для старых версий Android используем deprecated API с обработкой
@Suppress("DEPRECATION")
val wifiInfo = wifi.connectionInfo
@Suppress("DEPRECATION")
val ipInt = wifiInfo.ipAddress
if (ipInt != 0) {
val ip = String.format(
"%d.%d.%d.%d",
ipInt and 0xff,
ipInt shr 8 and 0xff,
ipInt shr 16 and 0xff,
ipInt shr 24 and 0xff
)
Logger.step("IP_DETECTION", "Legacy WiFi IP detected: $ip")
return ip
}
}
}
}
// Метод 2: Через NetworkInterface (универсальный метод)
val interfaces = NetworkInterface.getNetworkInterfaces()
for (networkInterface in interfaces) {
// Пропускаем loopback и неактивные интерфейсы
if (networkInterface.isLoopback || !networkInterface.isUp) continue
val addresses = networkInterface.inetAddresses
for (address in addresses) {
// Пропускаем IPv6 и loopback адреса
if (!address.isLoopbackAddress && address is java.net.Inet4Address) {
val ip = address.hostAddress
if (ip != null && !ip.startsWith("127.")) {
Logger.step("IP_DETECTION", "Network interface IP detected: $ip on ${networkInterface.name}")
return ip
}
}
}
}
// Метод 3: Через ConnectivityManager (Android 6+)
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
connectivityManager?.let { cm ->
val activeNetwork = cm.activeNetwork
val networkCapabilities = cm.getNetworkCapabilities(activeNetwork)
if (networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true) {
Logger.step("IP_DETECTION", "Active network detected but couldn't extract IP")
}
}
} catch (e: Exception) {
Logger.error("IP_DETECTION_ERROR", "Failed to detect local IP", e)
}
// Fallback: возвращаем IP по умолчанию для вашей подсети
val fallbackIP = "192.168.219.108" // ИСПРАВЛЕНИЕ: Изменено на ваш сервер
Logger.step("IP_DETECTION_FALLBACK", "Using fallback IP: $fallbackIP")
return fallbackIP
}
/**
* НОВОЕ: Получение IP сервера из URL
*/
fun extractServerIP(serverUrl: String): String {
return try {
val regex = """://([^:/]+)""".toRegex()
val match = regex.find(serverUrl)
match?.groupValues?.get(1) ?: "192.168.219.1" // Fallback на ваш роутер
} catch (e: Exception) {
Logger.error("SERVER_IP_EXTRACTION_ERROR", "Failed to extract server IP from URL: $serverUrl", e)
"192.168.219.1"
}
}
/**
* НОВОЕ: Автоматическое определение подсети
*/
fun Context.detectNetworkSubnet(): String {
val localIP = getLocalIpAddress()
return try {
val parts = localIP.split(".")
if (parts.size >= 3) {
"${parts[0]}.${parts[1]}.${parts[2]}.0/24"
} else {
"192.168.219.0/24" // Fallback на вашу подсеть
}
} catch (e: Exception) {
Logger.error("SUBNET_DETECTION_ERROR", "Failed to detect subnet", e)
"192.168.219.0/24"
}
}
/**
* НОВОЕ: Проверка, находится ли IP в локальной сети
*/
fun isLocalNetworkIP(ip: String): Boolean {
return try {
val parts = ip.split(".").map { it.toInt() }
when {
// 192.168.x.x
parts[0] == 192 && parts[1] == 168 -> true
// 10.x.x.x
parts[0] == 10 -> true
// 172.16.x.x - 172.31.x.x
parts[0] == 172 && parts[1] in 16..31 -> true
else -> false
}
} catch (e: Exception) {
false
}
}

View File

@@ -26,6 +26,23 @@ object Logger {
throwable?.printStackTrace()
}
// Методы для совместимости с WebRTCManager
fun debug(tag: String, message: String) {
Log.d(tag, "🔍 $message")
println("🔍 [DEBUG] [$tag] $message")
}
fun info(tag: String, message: String) {
Log.i(tag, " $message")
println(" [INFO] [$tag] $message")
}
fun error(tag: String, message: String, throwable: Throwable? = null) {
Log.e(tag, "$message", throwable)
println("❌ [ERROR] [$tag] $message")
throwable?.printStackTrace()
}
fun step(stepName: String, message: String) {
Log.d(TAG, "📋 STEP [$stepName]: $message")
println("📋 STEP [$stepName]: $message")
@@ -55,10 +72,4 @@ object Logger {
Log.d(TAG, "🌍 NETWORK: $message")
println("🌍 NETWORK: $message")
}
fun error(step: String, message: String, throwable: Throwable? = null) {
Log.e(TAG, "💥 ERROR in [$step]: $message", throwable)
println("💥 ERROR in [$step]: $message")
throwable?.printStackTrace()
}
}

View File

@@ -0,0 +1,58 @@
package com.example.godeye.utils
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
/**
* Утилиты для управления разрешениями
*/
class PermissionHelper(private val activity: ComponentActivity) {
companion object {
val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.WAKE_LOCK,
Manifest.permission.POST_NOTIFICATIONS
)
}
private val permissionLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.values.all { it }
onPermissionResult?.invoke(allGranted)
}
private var onPermissionResult: ((Boolean) -> Unit)? = null
/**
* Проверка всех необходимых разрешений
*/
fun hasAllPermissions(): Boolean {
return REQUIRED_PERMISSIONS.all { permission ->
ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED
}
}
/**
* Запрос разрешений
*/
fun requestPermissions(onResult: (Boolean) -> Unit) {
onPermissionResult = onResult
permissionLauncher.launch(REQUIRED_PERMISSIONS)
}
/**
* Проверка конкретного разрешения
*/
fun hasPermission(permission: String): Boolean {
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED
}
}

View File

@@ -0,0 +1,185 @@
package com.example.godeye.utils
import android.content.Context
import android.content.SharedPreferences
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Менеджер настроек приложения
*/
class PreferenceManager(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("godeye_settings", Context.MODE_PRIVATE)
companion object {
private const val TAG = "PreferenceManager"
private const val PREFS_NAME = "godeye_preferences"
private const val KEY_DEVICE_ID = "device_id"
private const val KEY_SERVER_URL = "server_url"
private const val KEY_AUTO_APPROVAL = "auto_approval"
private const val KEY_OPERATOR_MODE = "operator_mode"
private const val KEY_LAST_CONNECTION = "last_connection"
private const val KEY_STATISTICS_ENABLED = "statistics_enabled"
private const val KEY_DEVICE_NAME = "device_name"
private const val KEY_UUID_MIGRATED = "uuid_migrated"
private const val KEY_AUTO_CONNECT = "auto_connect"
private const val KEY_AUTO_APPROVE = "auto_approve"
private const val KEY_NOTIFICATIONS_ENABLED = "notifications_enabled"
private const val KEY_CAMERA_QUALITY = "camera_quality"
// ИСПРАВЛЕНИЕ: Обновляем IP по умолчанию на ваш сервер
private const val DEFAULT_SERVER_URL = "http://192.168.219.108:3001"
private const val DEFAULT_DEVICE_NAME = "Android Device"
}
// StateFlow для реактивного наблюдения за изменениями настроек
private val _serverUrl = MutableStateFlow(getServerUrl())
val serverUrl: StateFlow<String> = _serverUrl.asStateFlow()
private val _deviceName = MutableStateFlow(getDeviceName())
val deviceName: StateFlow<String> = _deviceName.asStateFlow()
private val _autoConnect = MutableStateFlow(getAutoConnect())
val autoConnect: StateFlow<Boolean> = _autoConnect.asStateFlow()
private val _autoApprove = MutableStateFlow(getAutoApprove())
val autoApprove: StateFlow<Boolean> = _autoApprove.asStateFlow()
// URL сервера
fun getServerUrl(): String = prefs.getString(KEY_SERVER_URL, DEFAULT_SERVER_URL) ?: DEFAULT_SERVER_URL
fun setServerUrl(url: String) {
prefs.edit().putString(KEY_SERVER_URL, url).apply()
_serverUrl.value = url
}
// Имя устройства
fun getDeviceName(): String = prefs.getString(KEY_DEVICE_NAME, DEFAULT_DEVICE_NAME) ?: DEFAULT_DEVICE_NAME
fun setDeviceName(name: String) {
prefs.edit().putString(KEY_DEVICE_NAME, name).apply()
_deviceName.value = name
}
// Автоматическое подключение
fun getAutoConnect(): Boolean = prefs.getBoolean(KEY_AUTO_CONNECT, false)
fun setAutoConnect(enabled: Boolean) {
prefs.edit().putBoolean(KEY_AUTO_CONNECT, enabled).apply()
_autoConnect.value = enabled
}
// Автоматическое подтверждение
fun getAutoApprove(): Boolean = prefs.getBoolean(KEY_AUTO_APPROVE, false)
fun setAutoApprove(enabled: Boolean) {
prefs.edit().putBoolean(KEY_AUTO_APPROVE, enabled).apply()
_autoApprove.value = enabled
}
// ID устройства (генерируется один раз) - теперь использует UUID
fun getDeviceId(): String {
var deviceId = prefs.getString(KEY_DEVICE_ID, null)
if (deviceId == null) {
// Генерируем настоящий UUID вместо timestamp
deviceId = java.util.UUID.randomUUID().toString()
prefs.edit().putString(KEY_DEVICE_ID, deviceId).apply()
}
return deviceId
}
/**
* Принудительная перегенерация Device ID с новым UUID
* Используется для обновления старых устройств на новый формат
*/
fun regenerateDeviceId(): String {
val newDeviceId = java.util.UUID.randomUUID().toString()
prefs.edit().putString(KEY_DEVICE_ID, newDeviceId).apply()
return newDeviceId
}
/**
* Проверка, является ли Device ID старым форматом (android_timestamp)
*/
fun isLegacyDeviceId(): Boolean {
val deviceId = prefs.getString(KEY_DEVICE_ID, null)
return deviceId != null && deviceId.startsWith("android_")
}
/**
* Автоматическое обновление старого Device ID на UUID
*/
fun migrateToUUID(): String {
return if (isLegacyDeviceId()) {
val oldDeviceId = prefs.getString(KEY_DEVICE_ID, null)
val newDeviceId = regenerateDeviceId()
android.util.Log.d("PreferenceManager", "Migrated Device ID from $oldDeviceId to $newDeviceId")
newDeviceId
} else {
getDeviceId()
}
}
// Настройки уведомлений
fun getNotificationsEnabled(): Boolean = prefs.getBoolean(KEY_NOTIFICATIONS_ENABLED, true)
fun setNotificationsEnabled(enabled: Boolean) {
prefs.edit().putBoolean(KEY_NOTIFICATIONS_ENABLED, enabled).apply()
}
// Качество камеры
fun getCameraQuality(): String = prefs.getString(KEY_CAMERA_QUALITY, "HIGH") ?: "HIGH"
fun setCameraQuality(quality: String) {
prefs.edit().putString(KEY_CAMERA_QUALITY, quality).apply()
}
/**
* Получить все настройки для отладки
*/
fun getAllSettings(): Map<String, Any> {
return mapOf(
"serverUrl" to getServerUrl(),
"deviceName" to getDeviceName(),
"autoConnect" to getAutoConnect(),
"autoApprove" to getAutoApprove(),
"deviceId" to getDeviceId(),
"notificationsEnabled" to getNotificationsEnabled(),
"cameraQuality" to getCameraQuality()
)
}
/**
* Очистка всех настроек (для отладки)
*/
fun clearAllSettings() {
prefs.edit().clear().apply()
android.util.Log.d("PreferenceManager", "All settings cleared")
}
/**
* Проверка времени последней миграции Device ID
*/
private fun shouldMigrate(): Boolean {
val lastMigration = prefs.getLong("last_migration_time", 0)
val currentTime = System.currentTimeMillis()
val migrationInterval = 24 * 60 * 60 * 1000L // 24 часа
return (currentTime - lastMigration) > migrationInterval
}
/**
* Автоматическая миграция с проверкой времени
*/
fun autoMigrateDeviceId(): String {
return if (isLegacyDeviceId() && shouldMigrate()) {
val newDeviceId = regenerateDeviceId()
prefs.edit().putLong("last_migration_time", System.currentTimeMillis()).apply()
newDeviceId
} else {
getDeviceId()
}
}
}

View File

@@ -1,646 +0,0 @@
package com.example.godeye.webrtc
import android.content.Context
import com.example.godeye.utils.Logger
import org.webrtc.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONObject
/**
* WebRTCManager - обработка WebRTC соединений согласно ТЗ
* Архитектура: Socket.IO для сигнализации, WebRTC для P2P медиа-потоков
*/
class WebRTCManager(
private val context: Context,
private val onSignalingMessage: (message: JSONObject) -> Unit
) {
private var peerConnectionFactory: PeerConnectionFactory? = null
private val activePeerConnections = mutableMapOf<String, PeerConnection>()
private var localVideoTrack: VideoTrack? = null
private var localAudioTrack: AudioTrack? = null
private var videoCapturer: CameraVideoCapturer? = null
// Состояния соединения
private val _connectionState = MutableStateFlow<Map<String, PeerConnection.PeerConnectionState>>(emptyMap())
val connectionState: StateFlow<Map<String, PeerConnection.PeerConnectionState>> = _connectionState.asStateFlow()
// ICE серверы согласно ТЗ
private val iceServers = listOf(
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()
)
// Конфигурация RTCConfiguration согласно ТЗ
private val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
}
init {
Logger.step("WEBRTC_INIT", "Initializing WebRTC Manager according to ТЗ")
initializePeerConnectionFactory()
}
/**
* Инициализация PeerConnectionFactory согласно ТЗ
*/
private fun initializePeerConnectionFactory() {
Logger.step("WEBRTC_FACTORY_INIT", "Initializing PeerConnectionFactory")
try {
val initializationOptions = PeerConnectionFactory.InitializationOptions.builder(context)
.setEnableInternalTracer(false)
.createInitializationOptions()
PeerConnectionFactory.initialize(initializationOptions)
val options = PeerConnectionFactory.Options()
peerConnectionFactory = PeerConnectionFactory.builder()
.setOptions(options)
.createPeerConnectionFactory()
Logger.step("WEBRTC_FACTORY_READY", "PeerConnectionFactory initialized successfully")
} catch (e: Exception) {
Logger.error("WEBRTC_FACTORY_ERROR", "Failed to initialize PeerConnectionFactory", e)
}
}
/**
* Начало стриминга для сессии - создание offer согласно ТЗ
*/
fun startStreaming(sessionId: String, cameraType: String) {
Logger.step("WEBRTC_START_STREAMING", "Starting WebRTC streaming for session: $sessionId, camera: $cameraType")
try {
val peerConnection = createPeerConnection(sessionId)
if (peerConnection == null) {
Logger.error("WEBRTC_START_ERROR", "Failed to create peer connection", null)
return
}
// Добавление локальных медиа-потоков
addLocalStreams(peerConnection, cameraType)
// Создание offer согласно ТЗ
createOffer(sessionId, peerConnection)
} catch (e: Exception) {
Logger.error("WEBRTC_START_ERROR", "Failed to start WebRTC streaming", e)
}
}
/**
* Создание PeerConnection для сессии
*/
private fun createPeerConnection(sessionId: String): PeerConnection? {
val factory = peerConnectionFactory ?: return null
val observer = object : PeerConnection.Observer {
override fun onIceCandidate(candidate: IceCandidate) {
Logger.step("WEBRTC_ICE_CANDIDATE", "ICE candidate for session: $sessionId")
// Отправка ICE candidate через SocketService согласно ТЗ
val message = JSONObject().apply {
put("type", "ice-candidate")
put("sessionId", sessionId)
put("candidate", candidate.sdp)
put("sdpMid", candidate.sdpMid)
put("sdpMLineIndex", candidate.sdpMLineIndex)
}
onSignalingMessage(message)
}
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState) {
Logger.step("WEBRTC_CONNECTION_CHANGE", "Session $sessionId state: $newState")
val currentStates = _connectionState.value.toMutableMap()
currentStates[sessionId] = newState
_connectionState.value = currentStates
}
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) {
Logger.step("WEBRTC_ICE_CONNECTION", "Session $sessionId ICE state: $newState")
}
override fun onIceConnectionReceivingChange(receiving: Boolean) {
Logger.step("WEBRTC_ICE_RECEIVING", "Session $sessionId ICE receiving: $receiving")
}
override fun onAddStream(stream: MediaStream) {
Logger.step("WEBRTC_STREAM_ADDED", "Remote stream added for session: $sessionId")
}
override fun onRemoveStream(stream: MediaStream) {
Logger.step("WEBRTC_STREAM_REMOVED", "Remote stream removed for session: $sessionId")
}
override fun onDataChannel(dataChannel: DataChannel) {
Logger.step("WEBRTC_DATA_CHANNEL", "Data channel opened for session: $sessionId")
}
override fun onRenegotiationNeeded() {
Logger.step("WEBRTC_RENEGOTIATION", "Renegotiation needed for session: $sessionId")
}
override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) {
Logger.step("WEBRTC_ICE_GATHERING", "Session $sessionId ICE gathering: $newState")
}
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {
Logger.step("WEBRTC_ICE_REMOVED", "ICE candidates removed for session: $sessionId")
}
override fun onSignalingChange(newState: PeerConnection.SignalingState) {
Logger.step("WEBRTC_SIGNALING", "Session $sessionId signaling: $newState")
}
}
val peerConnection = factory.createPeerConnection(rtcConfig, observer)
if (peerConnection != null) {
activePeerConnections[sessionId] = peerConnection
}
return peerConnection
}
/**
* Добавление локальных медиа-потоков (видео + аудио)
*/
private fun addLocalStreams(peerConnection: PeerConnection, cameraType: String) {
try {
// Создание локального видео-потока
if (localVideoTrack == null) {
localVideoTrack = createVideoTrack(cameraType)
}
// Создание локального аудио-потока
if (localAudioTrack == null) {
localAudioTrack = createAudioTrack()
}
// Добавление потоков в PeerConnection
localVideoTrack?.let { peerConnection.addTrack(it, listOf("stream")) }
localAudioTrack?.let { peerConnection.addTrack(it, listOf("stream")) }
Logger.step("WEBRTC_STREAMS_ADDED", "Local media streams added to peer connection")
} catch (e: Exception) {
Logger.error("WEBRTC_STREAMS_ERROR", "Failed to add local streams", e)
}
}
/**
* Создание видео-трека с указанной камерой
*/
private fun createVideoTrack(cameraType: String): VideoTrack? {
Logger.step("WEBRTC_VIDEO_TRACK", "Creating video track for camera: $cameraType")
try {
val factory = peerConnectionFactory ?: return null
// Создание видео источника
val videoSource = factory.createVideoSource(false)
// Создание захватчика камеры
videoCapturer = createCameraCapturer(cameraType)
if (videoCapturer == null) {
Logger.error("WEBRTC_VIDEO_ERROR", "Failed to create camera capturer", null)
return null
}
// Инициализация захвата видео
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", null)
videoCapturer?.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
// Запуск захвата видео с разрешением 1280x720 и 30 FPS
videoCapturer?.startCapture(1280, 720, 30)
// Создание видео-трека
val videoTrack = factory.createVideoTrack("video_track", videoSource)
Logger.step("WEBRTC_VIDEO_TRACK_CREATED", "Video track created successfully for camera: $cameraType")
return videoTrack
} catch (e: Exception) {
Logger.error("WEBRTC_VIDEO_TRACK_ERROR", "Failed to create video track", e)
return null
}
}
/**
* Создание захватчика камеры для указанного типа
*/
private fun createCameraCapturer(cameraType: String): CameraVideoCapturer? {
try {
val camera1Enumerator = Camera1Enumerator(false)
val camera2Enumerator = Camera2Enumerator(context)
val enumerator = if (Camera2Enumerator.isSupported(context)) camera2Enumerator else camera1Enumerator
// Поиск камеры по типу
val deviceNames = enumerator.deviceNames
for (deviceName in deviceNames) {
val isFrontFacing = enumerator.isFrontFacing(deviceName)
val isBackFacing = enumerator.isBackFacing(deviceName)
val matches = when (cameraType) {
"front" -> isFrontFacing
"back", "ultra_wide", "telephoto" -> isBackFacing
else -> isBackFacing
}
if (matches) {
Logger.d("Using camera device: $deviceName for type: $cameraType")
return enumerator.createCapturer(deviceName, null)
}
}
// Fallback к первой доступной камере
if (deviceNames.isNotEmpty()) {
Logger.d("Using fallback camera: ${deviceNames[0]}")
return enumerator.createCapturer(deviceNames[0], null)
}
Logger.error("CAMERA_CAPTURER_ERROR", "No camera devices found", null)
return null
} catch (e: Exception) {
Logger.error("CAMERA_CAPTURER_ERROR", "Failed to create camera capturer", e)
return null
}
}
/**
* Создание аудио-трека
*/
private fun createAudioTrack(): AudioTrack? {
try {
val factory = peerConnectionFactory ?: return null
// Создание аудио источника
val audioConstraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))
mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true"))
mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true"))
}
val audioSource = factory.createAudioSource(audioConstraints)
val audioTrack = factory.createAudioTrack("audio_track", audioSource)
Logger.step("WEBRTC_AUDIO_TRACK_CREATED", "Audio track created successfully")
return audioTrack
} catch (e: Exception) {
Logger.error("WEBRTC_AUDIO_TRACK_ERROR", "Failed to create audio track", e)
return null
}
}
/**
* Создание offer для сессии
*/
private fun createOffer(sessionId: String, peerConnection: PeerConnection) {
try {
val constraints = MediaConstraints().apply {
// Исправляем настройки для корректной работы WebRTC
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"))
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"))
// Добавляем дополнительные constraints для стабильности
mandatory.add(MediaConstraints.KeyValuePair("VoiceActivityDetection", "true"))
}
peerConnection.createOffer(object : SdpObserver {
override fun onCreateSuccess(sessionDescription: SessionDescription) {
Logger.step("WEBRTC_OFFER_CREATED", "Offer created for session: $sessionId")
// Изменяем SDP для исправления проблемы с m-section
val modifiedSdp = modifySdpForCompatibility(sessionDescription.description)
val modifiedSessionDescription = SessionDescription(sessionDescription.type, modifiedSdp)
peerConnection.setLocalDescription(object : SdpObserver {
override fun onSetSuccess() {
Logger.step("WEBRTC_LOCAL_DESC_SET", "Local description set successfully for session: $sessionId")
// Отправка offer через SocketService согласно ТЗ
val message = JSONObject().apply {
put("type", "offer")
put("sessionId", sessionId)
put("sdp", modifiedSdp)
}
onSignalingMessage(message)
}
override fun onSetFailure(error: String) {
Logger.error("WEBRTC_SET_LOCAL_ERROR", "Failed to set local description: $error", null)
// Не крашим приложение, а пытаемся создать новое соединение
handleWebRTCError(sessionId, "Local description error: $error")
}
override fun onCreateSuccess(p0: SessionDescription?) {}
override fun onCreateFailure(p0: String?) {}
}, modifiedSessionDescription)
}
override fun onCreateFailure(error: String) {
Logger.error("WEBRTC_OFFER_ERROR", "Failed to create offer: $error", null)
handleWebRTCError(sessionId, "Offer creation error: $error")
}
override fun onSetSuccess() {}
override fun onSetFailure(error: String?) {}
}, constraints)
} catch (e: Exception) {
Logger.error("WEBRTC_OFFER_EXCEPTION", "Exception creating offer", e)
handleWebRTCError(sessionId, "Offer exception: ${e.message}")
}
}
/**
* Модификация SDP для совместимости
*/
private fun modifySdpForCompatibility(originalSdp: String): String {
try {
Logger.d("Original SDP length: ${originalSdp.length}")
// Для send-only режима создаем минимальный корректный SDP
val lines = originalSdp.split("\r\n").toMutableList()
val modifiedLines = mutableListOf<String>()
var inVideoSection = false
var inAudioSection = false
var currentSection = ""
for (line in lines) {
when {
line.startsWith("v=") -> {
modifiedLines.add(line)
}
line.startsWith("o=") -> {
modifiedLines.add(line)
}
line.startsWith("s=") -> {
modifiedLines.add(line)
}
line.startsWith("t=") -> {
modifiedLines.add(line)
}
line.startsWith("a=group:BUNDLE") -> {
modifiedLines.add("a=group:BUNDLE 0 1")
}
line.startsWith("a=msid-semantic") -> {
modifiedLines.add(line)
}
line.startsWith("m=video") -> {
inVideoSection = true
inAudioSection = false
currentSection = "video"
modifiedLines.add(line)
}
line.startsWith("m=audio") -> {
inVideoSection = false
inAudioSection = true
currentSection = "audio"
modifiedLines.add(line)
}
line.startsWith("c=") -> {
modifiedLines.add(line)
}
line.startsWith("a=mid:") -> {
when (currentSection) {
"video" -> modifiedLines.add("a=mid:0")
"audio" -> modifiedLines.add("a=mid:1")
else -> modifiedLines.add(line)
}
}
line.startsWith("a=sendonly") || line.startsWith("a=sendrecv") || line.startsWith("a=recvonly") -> {
modifiedLines.add("a=sendonly")
}
line.startsWith("a=rtcp-mux") && !line.contains("only") -> {
modifiedLines.add(line)
}
line.startsWith("a=rtpmap:") ||
line.startsWith("a=fmtp:") ||
line.startsWith("a=ssrc:") ||
line.startsWith("a=msid:") ||
line.startsWith("a=cname:") ||
line.startsWith("a=ice-ufrag:") ||
line.startsWith("a=ice-pwd:") ||
line.startsWith("a=fingerprint:") ||
line.startsWith("a=setup:") ||
line.startsWith("a=candidate:") -> {
modifiedLines.add(line)
}
// Пропускаем проблемные RTCP feedback атрибуты для send-only
line.startsWith("a=rtcp-fb:") ||
line.startsWith("a=rtcp-mux-only") ||
line.startsWith("a=rtcp-rsize") -> {
// Пропускаем эти строки
}
line.trim().isEmpty() -> {
// Пропускаем пустые строки
}
else -> {
// Добавляем остальные атрибуты
modifiedLines.add(line)
}
}
}
val modifiedSdp = modifiedLines.joinToString("\r\n")
Logger.d("Modified SDP length: ${modifiedSdp.length}")
Logger.d("SDP modifications applied successfully")
return modifiedSdp
} catch (e: Exception) {
Logger.error("SDP_MODIFY_ERROR", "Failed to modify SDP", e)
return originalSdp
}
}
/**
* Обработка WebRTC ошибок без краша приложения
*/
private fun handleWebRTCError(sessionId: String, error: String) {
Logger.error("WEBRTC_ERROR_HANDLED", "WebRTC error for session $sessionId: $error", null)
// Уведомляем о проблеме через сигналинг
val errorMessage = JSONObject().apply {
put("type", "error")
put("sessionId", sessionId)
put("error", error)
}
try {
onSignalingMessage(errorMessage)
} catch (e: Exception) {
Logger.error("WEBRTC_ERROR_SIGNALING", "Failed to send error message", e)
}
}
/**
* Обработка answer от оператора
*/
fun handleAnswer(sessionId: String, answerSdp: String) {
Logger.step("WEBRTC_ANSWER", "Processing WebRTC answer for session: $sessionId")
val peerConnection = activePeerConnections[sessionId]
if (peerConnection == null) {
Logger.error("WEBRTC_ANSWER_ERROR", "No peer connection found for session: $sessionId", null)
return
}
try {
val answer = SessionDescription(SessionDescription.Type.ANSWER, answerSdp)
peerConnection.setRemoteDescription(object : SdpObserver {
override fun onSetSuccess() {
Logger.step("WEBRTC_ANSWER_SET", "Answer set successfully for session: $sessionId")
}
override fun onSetFailure(error: String) {
Logger.error("WEBRTC_ANSWER_SET_ERROR", "Failed to set answer: $error", null)
}
override fun onCreateSuccess(p0: SessionDescription?) {}
override fun onCreateFailure(p0: String?) {}
}, answer)
} catch (e: Exception) {
Logger.error("WEBRTC_ANSWER_EXCEPTION", "Exception handling answer", e)
}
}
/**
* Обработка offer от оператора (не используется в текущей архитектуре)
*/
@Suppress("UNUSED_PARAMETER")
fun handleOffer(sessionId: String, offerSdp: String) {
Logger.step("WEBRTC_OFFER", "Processing WebRTC offer for session: $sessionId")
// Пока не реализовано - Android устройство только отправляет offer
}
/**
* Обработка ICE кандидата от оператора
*/
fun handleIceCandidate(sessionId: String, candidateSdp: String, sdpMid: String, sdpMLineIndex: Int) {
Logger.step("WEBRTC_ICE", "Processing ICE candidate for session: $sessionId")
val peerConnection = activePeerConnections[sessionId]
if (peerConnection == null) {
Logger.error("WEBRTC_ICE_ERROR", "No peer connection found for session: $sessionId", null)
return
}
try {
val iceCandidate = IceCandidate(sdpMid, sdpMLineIndex, candidateSdp)
peerConnection.addIceCandidate(iceCandidate)
Logger.step("WEBRTC_ICE_ADDED", "ICE candidate added for session: $sessionId")
} catch (e: Exception) {
Logger.error("WEBRTC_ICE_EXCEPTION", "Exception handling ICE candidate", e)
}
}
/**
* Переключение камеры
*/
fun switchCamera(cameraType: String) {
Logger.step("WEBRTC_SWITCH_CAMERA", "Switching camera to: $cameraType")
try {
// Остановка текущего захвата
videoCapturer?.stopCapture()
// Создание нового видео-трека
localVideoTrack?.dispose()
localVideoTrack = createVideoTrack(cameraType)
// Обновление треков во всех активных соединениях
activePeerConnections.forEach { (_, peerConnection) ->
localVideoTrack?.let { videoTrack ->
// Удаление старого трека и добавление нового
val senders = peerConnection.senders
senders.forEach { sender ->
if (sender.track()?.kind() == "video") {
sender.setTrack(videoTrack, false)
}
}
}
}
Logger.step("WEBRTC_CAMERA_SWITCHED", "Camera switched to: $cameraType")
} catch (e: Exception) {
Logger.error("WEBRTC_SWITCH_ERROR", "Failed to switch camera", e)
}
}
/**
* Завершение сессии
*/
fun endSession(sessionId: String) {
Logger.step("WEBRTC_END_SESSION", "Ending WebRTC session: $sessionId")
activePeerConnections[sessionId]?.let { peerConnection ->
peerConnection.close()
activePeerConnections.remove(sessionId)
}
val currentStates = _connectionState.value.toMutableMap()
currentStates.remove(sessionId)
_connectionState.value = currentStates
}
/**
* Остановка всех стримов
*/
fun stopAllStreaming() {
Logger.step("WEBRTC_STOP_ALL", "Stopping all WebRTC streaming")
activePeerConnections.forEach { (_, peerConnection) ->
peerConnection.close()
}
activePeerConnections.clear()
videoCapturer?.stopCapture()
videoCapturer?.dispose()
videoCapturer = null
localVideoTrack?.dispose()
localVideoTrack = null
localAudioTrack?.dispose()
localAudioTrack = null
_connectionState.value = emptyMap()
}
/**
* Освобождение ресурсов
*/
fun dispose() {
Logger.step("WEBRTC_DISPOSE", "Disposing WebRTC Manager")
stopAllStreaming()
peerConnectionFactory?.dispose()
peerConnectionFactory = null
}
}
/**
* SdpObserver по умолчанию для упрощения кода
*/
open class SimpleSdpObserver : SdpObserver {
override fun onCreateSuccess(sessionDescription: SessionDescription) {}
override fun onSetSuccess() {}
override fun onCreateFailure(error: String) {}
override fun onSetFailure(error: String) {}
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Основной градиентный фон в стиле GodEye -->
<item>
<shape>
<gradient
android:startColor="#0a1828"
android:centerColor="#1e3a5f"
android:endColor="#2d5a87"
android:angle="135" />
</shape>
</item>
<!-- Дополнительный слой с точечным паттерном -->
<item>
<shape>
<gradient
android:startColor="#00000000"
android:centerColor="#1a000000"
android:endColor="#33000000"
android:angle="45" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@color/button_danger_pressed" />
<corners android:radius="8dp" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<solid android:color="@color/button_secondary" />
<corners android:radius="8dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/button_danger" />
<corners android:radius="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@color/button_primary_pressed" />
<corners android:radius="8dp" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<solid android:color="@color/button_secondary" />
<corners android:radius="8dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/button_primary" />
<corners android:radius="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@color/button_secondary_pressed" />
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="@color/panel_border" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<solid android:color="#404040" />
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="@color/panel_border" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/button_secondary" />
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="@color/panel_border" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/status_idle" />
<size
android:width="20dp"
android:height="20dp" />
</shape>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Основная иконка камеры -->
<path
android:fillColor="@color/success_green"
android:pathData="M17,10.5V7A1,1 0,0 0,16 6H4A1,1 0,0 0,3 7V17A1,1 0,0 0,4 18H16A1,1 0,0 0,17 17V13.5L21,17.5V6.5L17,10.5Z"/>
<!-- Красный индикатор записи -->
<path
android:fillColor="@color/error_red"
android:pathData="M19,2A3,3 0,1 1,19 8A3,3 0,1 1,19 2Z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/accent_blue"
android:pathData="M9,12l2,2l4,-4m5.618,-4.016A11.955,11.955 0,0 1,12 2.944A11.955,11.955 0,0 1,2.382 5.984l1.158,1.158a10,10 0,0 0,17.8 0L20.618,5.984zM21,10v8a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2,-2V10l9,-5 9,5z"/>
</vector>

View File

@@ -1,170 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Основной градиентный фон -->
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="0"
android:startX="0"
android:endY="108"
android:endX="108"
android:type="linear">
<item android:offset="0" android:color="#0A1828"/>
<item android:offset="0.3" android:color="#1E3A5F"/>
<item android:offset="0.7" android:color="#2D5A87"/>
<item android:offset="1" android:color="#3A6EA5"/>
</gradient>
</aapt:attr>
</path>
<!-- Дополнительные элементы фона -->
<!-- Круговые линии для технологического эффекта (заменены на path) -->
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
android:pathData="M54,14A40,40 0,1 1,54,94A40,40 0,1 1,54,14Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1A40E0D0"
android:strokeWidth="1"/>
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
android:pathData="M54,4A50,50 0,1 1,54,104A50,50 0,1 1,54,4Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1A00BFFF"
android:strokeWidth="0.5"/>
<!-- Точки на фоне (заменены на path) -->
<path android:pathData="M20,19a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
<path android:pathData="M88,19a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
<path android:pathData="M20,87a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
<path android:pathData="M88,87a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
</vector>

View File

@@ -4,27 +4,80 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
<!-- Фоновый градиент -->
<path
android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="0"
android:startX="0"
android:endY="108"
android:endX="108"
android:type="linear">
<item android:offset="0" android:color="#0A1828"/>
<item android:offset="0.5" android:color="#1E3A5F"/>
<item android:offset="1" android:color="#2D5A87"/>
</gradient>
</aapt:attr>
</path>
<!-- Основной контур глаза -->
<path
android:fillColor="#00BFFF"
android:pathData="M54,30C42,30 32,40 32,52C32,64 42,74 54,74C66,74 76,64 76,52C76,40 66,30 54,30Z"/>
<!-- Внутренний контур глаза -->
<path
android:fillColor="#E0F4FF"
android:pathData="M54,35C45,35 37,43 37,52C37,61 45,69 54,69C63,69 71,61 71,52C71,43 63,35 54,35Z"/>
<!-- Зрачок -->
<path
android:fillColor="#0A1828"
android:pathData="M54,42C50,42 47,45 47,49C47,53 50,56 54,56C58,56 61,53 61,49C61,45 58,42 54,42Z"/>
<!-- Отражение в глазу -->
<path
android:fillColor="#FFFFFF"
android:pathData="M52,44C51,44 50,45 50,46C50,47 51,48 52,48C53,48 54,47 54,46C54,45 53,44 52,44Z"/>
<!-- Технологические линии вокруг глаза -->
<!-- Верхние линии -->
<path
android:fillColor="#40E0D0"
android:strokeWidth="2"
android:strokeColor="#40E0D0"
android:pathData="M25,45 L30,45 M78,45 L83,45"/>
<path
android:fillColor="#40E0D0"
android:strokeWidth="2"
android:strokeColor="#40E0D0"
android:pathData="M25,52 L30,52 M78,52 L83,52"/>
<path
android:fillColor="#40E0D0"
android:strokeWidth="2"
android:strokeColor="#40E0D0"
android:pathData="M25,59 L30,59 M78,59 L83,59"/>
<!-- Точки на концах линий (заменены на path элементы) -->
<path
android:fillColor="#00FFFF"
android:pathData="M23,43a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
<path
android:fillColor="#00FFFF"
android:pathData="M23,50a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
<path
android:fillColor="#00FFFF"
android:pathData="M23,57a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
<path
android:fillColor="#00FFFF"
android:pathData="M85,43a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
<path
android:fillColor="#00FFFF"
android:pathData="M85,50a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
<path
android:fillColor="#00FFFF"
android:pathData="M85,57a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
</vector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/error_red"
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0" />
<path
android:fillColor="@color/white"
android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/panel_background" />
<corners android:radius="12dp" />
<stroke android:width="1dp" android:color="@color/panel_border" />
</shape>

View File

@@ -0,0 +1,256 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/background_dark">
<!-- Заголовок -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Мониторинг трансляции"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:gravity="center"
android:layout_marginBottom="24dp" />
<!-- Статус соединения -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Соединение:"
android:textSize="16sp"
android:textColor="@color/text_secondary" />
<TextView
android:id="@+id/connectionStatusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Не подключено"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/error_red" />
</LinearLayout>
<!-- Индикатор статуса трансляции -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Статус трансляции:"
android:textSize="16sp"
android:textColor="@color/text_secondary" />
<View
android:id="@+id/streamingStatusIndicator"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="8dp"
android:background="@drawable/circle_indicator"
android:backgroundTint="@color/status_idle" />
<TextView
android:id="@+id/statusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Готов к трансляции"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
</LinearLayout>
<!-- Время записи -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="32dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Время записи:"
android:textSize="16sp"
android:textColor="@color/text_secondary" />
<ImageView
android:id="@+id/recordingIndicator"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_recording_active"
android:visibility="gone" />
<TextView
android:id="@+id/recordingTimeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00:00"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:textColor="@color/accent_blue" />
</LinearLayout>
<!-- Разделитель -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider"
android:layout_marginBottom="24dp" />
<!-- Кнопки управления -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
<!-- Первая строка кнопок -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<Button
android:id="@+id/startButton"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Начать трансляцию"
android:textSize="14sp"
android:textStyle="bold"
android:background="@drawable/button_primary"
android:textColor="@color/white"
android:elevation="4dp" />
<Button
android:id="@+id/stopButton"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Остановить"
android:textSize="14sp"
android:textStyle="bold"
android:background="@drawable/button_danger"
android:textColor="@color/white"
android:elevation="4dp"
android:enabled="false" />
</LinearLayout>
<!-- Вторая строка кнопок -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="24dp">
<Button
android:id="@+id/switchCameraButton"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Переключить камеру"
android:textSize="12sp"
android:background="@drawable/button_secondary"
android:textColor="@color/text_primary"
android:elevation="2dp"
android:enabled="false" />
<Button
android:id="@+id/disconnectButton"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Отключиться"
android:textSize="12sp"
android:background="@drawable/button_secondary"
android:textColor="@color/text_primary"
android:elevation="2dp" />
</LinearLayout>
</LinearLayout>
<!-- Информация о сессии -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@drawable/info_panel_background"
android:padding="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Информация о сессии"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/sessionInfoText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Сессия не активна"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Возможности:"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="• Видеопоток без предварительного просмотра\n• Счетчик времени записи\n• Переключение камер\n• Статус трансляции в реальном времени"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:lineSpacingExtra="2dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Оригинальные цвета Material Design -->
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
@@ -7,4 +8,67 @@
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Цветовая схема GodEye Signal Center -->
<!-- Основные цвета из предоставленных изображений -->
<color name="godeye_primary">#00BFFF</color> <!-- Яркий голубой/cyan -->
<color name="godeye_primary_variant">#0080FF</color> <!-- Темно-голубой -->
<color name="godeye_secondary">#40E0D0</color> <!-- Бирюзовый -->
<color name="godeye_accent">#00FFFF</color> <!-- Cyan accent -->
<!-- Фоновые цвета -->
<color name="godeye_background_dark">#0A1828</color> <!-- Темно-синий фон -->
<color name="godeye_background_medium">#1E3A5F</color> <!-- Средний синий -->
<color name="godeye_background_light">#2D5A87</color> <!-- Светлый синий -->
<!-- Поверхности и карточки -->
<color name="godeye_surface">#1A2B3D</color> <!-- Поверхность карточек -->
<color name="godeye_surface_variant">#243447</color> <!-- Вариант поверхности -->
<color name="godeye_on_surface">#E0F4FF</color> <!-- Текст на поверхности -->
<color name="godeye_on_surface_variant">#B0C4DE</color> <!-- Вторичный текст -->
<!-- Состояния -->
<color name="godeye_success">#00FF80</color> <!-- Успешное подключение -->
<color name="godeye_warning">#FFB000</color> <!-- Предупреждения -->
<color name="godeye_error">#FF4444</color> <!-- Ошибки -->
<!-- Прозрачности -->
<color name="godeye_overlay_light">#33FFFFFF</color> <!-- Светлая overlay -->
<color name="godeye_overlay_dark">#66000000</color> <!-- Темная overlay -->
<!-- Специальные эффекты -->
<color name="godeye_glow">#80BFFF</color> <!-- Свечение элементов -->
<color name="godeye_connection_active">#00FF7F</color> <!-- Активное соединение -->
<color name="godeye_connection_inactive">#778899</color> <!-- Неактивное соединение -->
<!-- Основные цвета интерфейса мониторинга трансляции -->
<color name="background_dark">#1E1E1E</color>
<color name="text_primary">#FFFFFF</color>
<color name="text_secondary">#B3B3B3</color>
<color name="divider">#333333</color>
<!-- Цвета статусов трансляции -->
<color name="status_idle">#6C757D</color>
<color name="status_connecting">#FFC107</color>
<color name="status_streaming">#28A745</color>
<color name="status_error">#DC3545</color>
<color name="status_stopping">#FF8C00</color>
<!-- Цвета состояний -->
<color name="success_green">#28A745</color>
<color name="error_red">#DC3545</color>
<color name="warning_yellow">#FFC107</color>
<color name="accent_blue">#007BFF</color>
<!-- Цвета кнопок -->
<color name="button_primary">#007BFF</color>
<color name="button_primary_pressed">#0056B3</color>
<color name="button_danger">#DC3545</color>
<color name="button_danger_pressed">#C82333</color>
<color name="button_secondary">#6C757D</color>
<color name="button_secondary_pressed">#5A6268</color>
<!-- Цвета панелей -->
<color name="panel_background">#2D2D2D</color>
<color name="panel_border">#404040</color>
</resources>

View File

@@ -1,7 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.GodEye" parent="Theme.AppCompat.DayNight.NoActionBar">
<style name="Theme.GodEye" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Основные цвета приложения -->
<item name="colorPrimary">@color/godeye_primary</item>
<item name="colorPrimaryVariant">@color/godeye_primary_variant</item>
<item name="colorSecondary">@color/godeye_secondary</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Фон приложения -->
<item name="android:windowBackground">@drawable/app_background</item>
<item name="colorSurface">@color/godeye_surface</item>
<item name="colorOnSurface">@color/godeye_on_surface</item>
<!-- Статус бар -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<!-- Навигационная панель -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- Дополнительные настройки -->
<item name="android:windowContentTransitions">true</item>
<item name="android:windowAllowEnterTransitionOverlap">true</item>
<item name="android:windowAllowReturnTransitionOverlap">true</item>
</style>
<!-- Тема для экрана заставки -->
<style name="Theme.GodEye.Splash" parent="Theme.GodEye">
<item name="android:windowFullscreen">true</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>

View File

@@ -0,0 +1,112 @@
package com.example.godeye.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/**
* Цветовая палитра GodEye согласно ТЗ
*/
object GodEyeColors {
// Основные цвета приложения
val BlackPure = Color(0xFF000000)
val BlackSoft = Color(0xFF1A1A1A)
val BlackMedium = Color(0xFF2D2D2D)
val IvoryPure = Color(0xFFFFFFF0)
val IvorySoft = Color(0xFFF5F5DC)
val IvoryMedium = Color(0xFFE6E6D4)
val NavyDark = Color(0xFF0F1419)
val NavyMedium = Color(0xFF1E2328)
val NavyLight = Color(0xFF2D3748)
// Функциональные цвета
val RecordRed = Color(0xFFFF3B30)
val WarningAmber = Color(0xFFFF9500)
val SuccessGreen = Color(0xFF30D158)
val InfoBlue = Color(0xFF007AFF)
// Градиенты
val PrimaryGradientStart = NavyDark
val PrimaryGradientEnd = BlackSoft
val AccentGradientStart = NavyLight
val AccentGradientEnd = NavyMedium
}
private val DarkColorScheme = darkColorScheme(
primary = GodEyeColors.NavyLight,
onPrimary = GodEyeColors.IvoryPure,
primaryContainer = GodEyeColors.NavyMedium,
onPrimaryContainer = GodEyeColors.IvorySoft,
secondary = GodEyeColors.IvoryMedium,
onSecondary = GodEyeColors.BlackPure,
secondaryContainer = GodEyeColors.BlackMedium,
onSecondaryContainer = GodEyeColors.IvoryPure,
tertiary = GodEyeColors.WarningAmber,
onTertiary = GodEyeColors.BlackPure,
error = GodEyeColors.RecordRed,
onError = GodEyeColors.IvoryPure,
background = GodEyeColors.BlackPure,
onBackground = GodEyeColors.IvoryPure,
surface = GodEyeColors.BlackSoft,
onSurface = GodEyeColors.IvoryPure,
surfaceVariant = GodEyeColors.BlackMedium,
onSurfaceVariant = GodEyeColors.IvorySoft,
outline = GodEyeColors.NavyMedium,
outlineVariant = GodEyeColors.NavyLight
)
private val LightColorScheme = lightColorScheme(
primary = GodEyeColors.NavyMedium,
onPrimary = GodEyeColors.IvoryPure,
primaryContainer = GodEyeColors.NavyLight,
onPrimaryContainer = GodEyeColors.BlackPure,
secondary = GodEyeColors.BlackMedium,
onSecondary = GodEyeColors.IvoryPure,
secondaryContainer = GodEyeColors.IvoryMedium,
onSecondaryContainer = GodEyeColors.BlackPure,
tertiary = GodEyeColors.WarningAmber,
onTertiary = GodEyeColors.IvoryPure,
error = GodEyeColors.RecordRed,
onError = GodEyeColors.IvoryPure,
background = GodEyeColors.IvoryPure,
onBackground = GodEyeColors.BlackPure,
surface = GodEyeColors.IvorySoft,
onSurface = GodEyeColors.BlackPure,
surfaceVariant = GodEyeColors.IvoryMedium,
onSurfaceVariant = GodEyeColors.BlackMedium,
outline = GodEyeColors.NavyLight,
outlineVariant = GodEyeColors.NavyMedium
)
@Composable
fun GodEyeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(),
content = content
)
}

266
build.log Normal file
View File

@@ -0,0 +1,266 @@
Configuration on demand is an incubating feature.
> Task :app:clean
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED
> Task :app:dataBindingMergeDependencyArtifactsDebug
> Task :app:generateDebugResValues FROM-CACHE
> Task :app:generateDebugResources FROM-CACHE
> Task :app:mergeDebugResources FROM-CACHE
> Task :app:packageDebugResources FROM-CACHE
> Task :app:processDebugNavigationResources FROM-CACHE
> Task :app:parseDebugLocalResources FROM-CACHE
> Task :app:dataBindingGenBaseClassesDebug FROM-CACHE
> Task :app:checkDebugAarMetadata
> Task :app:compileDebugNavigationResources FROM-CACHE
> Task :app:mapDebugSourceSetPaths
> Task :app:createDebugCompatibleScreenManifests
> Task :app:extractDeepLinksDebug FROM-CACHE
> Task :app:processDebugMainManifest FROM-CACHE
> Task :app:processDebugManifest FROM-CACHE
> Task :app:processDebugManifestForPackage FROM-CACHE
> Task :app:processDebugResources FROM-CACHE
> Task :app:javaPreCompileDebug FROM-CACHE
> Task :app:mergeDebugShaders
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets
> Task :app:compressDebugAssets FROM-CACHE
> Task :app:desugarDebugFileDependencies FROM-CACHE
> Task :app:checkDebugDuplicateClasses
> Task :app:mergeDebugJniLibFolders
> Task :app:mergeExtDexDebug FROM-CACHE
> Task :app:mergeLibDexDebug FROM-CACHE
> Task :app:mergeDebugNativeLibs NO-SOURCE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug
> Task :app:writeDebugAppMetadata
> Task :app:writeDebugSigningConfigVersions
> Task :app:compileDebugKotlin FAILED
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/GodEyeApplication.kt:4:33 Unresolved reference 'ErrorHandler'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/GodEyeApplication.kt:13:32 Unresolved reference 'ErrorHandler'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:12:26 Unresolved reference 'result'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:50:38 Unresolved reference 'registerForActivityResult'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:51:9 Unresolved reference 'ActivityResultContracts'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:52:9 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:53:38 Unresolved reference 'values'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:53:49 Unresolved reference 'it'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:53:51 Unresolved reference 'it'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:18:27 Unresolved reference 'webrtc'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:38:32 Unresolved reference 'WebRTCManager'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:141:37 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:141:45 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.models.ConnectionState>' was expected.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:141:47 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:149:36 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:149:44 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.models.CameraRequest?>' was expected.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:149:46 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:152:56 Unresolved reference 'operatorId'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:152:82 Unresolved reference 'cameraType'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:153:44 Assignment type mismatch: actual type is 'kotlin.Any', but 'com.example.godeye.models.CameraRequest?' was expected.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:160:34 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:160:42 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.services.WebRTCEvent?>' was expected.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:160:44 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:161:24 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:161:24 Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch:
fun <T, R> T.let(block: (T) -> R): R
[R|Contract description]
<
CallsInPlace(block, EXACTLY_ONCE)
>
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:161:28 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:167:37 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:167:45 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<kotlin.collections.Map<kotlin.String, com.example.godeye.models.CameraSession>>' was expected.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:167:47 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:168:44 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:168:44 Not enough information to infer type argument for 'V'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:168:57 Not enough information to infer type argument for 'V'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:168:68 Not enough information to infer type argument for 'V'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:172:46 Unresolved reference 'operatorId'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:173:46 Unresolved reference 'cameraType'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:174:46 Unresolved reference 'webRTCConnected'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:176:60 Unresolved reference 'startTime'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:180:47 Unresolved reference 'values'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:180:58 Unresolved reference 'isActive'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:180:60 Unresolved reference 'it'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:189:25 Unresolved reference 'WebRTCManager'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:189:50 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:191:82 Unresolved reference 'getString'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:260:32 Unresolved reference 'startStreaming'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:292:32 Unresolved reference 'handleOffer'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:296:32 Unresolved reference 'handleAnswer'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:300:32 Unresolved reference 'handleIceCandidate'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:317:32 Unresolved reference 'stopAllStreaming'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:338:32 Unresolved reference 'switchCamera'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:361:24 Unresolved reference 'endSession'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:374:24 Unresolved reference 'stopAllStreaming'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:415:37 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:415:45 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.models.ConnectionState>' was expected.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:415:47 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:422:36 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:422:44 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.models.CameraRequest?>' was expected.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:422:46 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:429:34 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:429:42 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.services.WebRTCEvent?>' was expected.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:429:44 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:430:24 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:430:24 Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch:
fun <T, R> T.let(block: (T) -> R): R
[R|Contract description]
<
CallsInPlace(block, EXACTLY_ONCE)
>
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:430:28 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:474:37 Unresolved reference 'WebRTCManager'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:474:62 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:481:32 Unresolved reference 'startStreaming'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:495:24 Unresolved reference 'dispose'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/managers/PermissionManager.kt:38:84 Unresolved reference 'UPSIDE_DOWN_CAKE'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/managers/PermissionManager.kt:39:42 Unresolved reference 'FOREGROUND_SERVICE_CAMERA'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/managers/PermissionManager.kt:196:33 Unresolved reference 'FOREGROUND_SERVICE_CAMERA'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:6:12 Unresolved reference 'webrtc'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:7:12 Unresolved reference 'webrtc'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:20:40 Unresolved reference 'PeerConnectionFactory'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:21:33 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:22:34 Unresolved reference 'VideoTrack'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:23:34 Unresolved reference 'AudioTrack'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:24:32 Unresolved reference 'CameraVideoCapturer'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:25:39 Unresolved reference 'SurfaceTextureHelper'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:27:36 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:27:53 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:28:36 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:28:91 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:33:30 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:34:9 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:34:9 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:35:9 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:43:27 Unresolved reference 'PeerConnectionFactory'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:46:9 Unresolved reference 'PeerConnectionFactory'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:48:33 Unresolved reference 'JavaAudioDeviceModule'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:50:33 Unresolved reference 'PeerConnectionFactory'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:52:37 Unresolved reference 'DefaultVideoEncoderFactory'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:53:17 Unresolved reference 'EglBase'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:54:37 Unresolved reference 'DefaultVideoDecoderFactory'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:54:64 Unresolved reference 'EglBase'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:78:28 Unresolved reference 'stopCapture'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:79:28 Unresolved reference 'dispose'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:81:30 Unresolved reference 'dispose'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:82:30 Unresolved reference 'dispose'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:84:29 Unresolved reference 'close'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:88:38 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:97:28 Unresolved reference 'CameraVideoCapturer'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:97:50 Unresolved reference 'switchCamera'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:101:22 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:101:66 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:101:72 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:102:13 Unresolved reference 'bundlePolicy'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:102:28 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:103:13 Unresolved reference 'rtcpMuxPolicy'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:103:29 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:106:49 Unresolved reference 'createPeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:106:87 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:107:13 'onSignalingChange' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:107:51 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:111:13 'onIceConnectionChange' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:111:55 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:115:13 'onConnectionChange' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:115:52 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:120:13 'onIceCandidate' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:120:52 Unresolved reference 'IceCandidate'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:123:48 Unresolved reference 'sdp'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:124:52 Unresolved reference 'sdpMLineIndex'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:125:45 Unresolved reference 'sdpMid'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:130:13 'onIceCandidatesRemoved' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:130:71 Unresolved reference 'IceCandidate'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:131:13 'onAddStream' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:131:46 Unresolved reference 'MediaStream'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:132:13 'onRemoveStream' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:132:49 Unresolved reference 'MediaStream'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:133:13 'onDataChannel' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:133:49 Unresolved reference 'DataChannel'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:134:13 'onRenegotiationNeeded' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:135:13 'onIceGatheringChange' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:135:54 Unresolved reference 'PeerConnection'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:136:13 'onIceConnectionReceivingChange' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:141:50 Unresolved reference 'createVideoSource'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:142:50 Unresolved reference 'createVideoTrack'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:144:50 Unresolved reference 'createAudioSource'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:144:68 Unresolved reference 'MediaConstraints'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:145:50 Unresolved reference 'createAudioTrack'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:147:45 Unresolved reference 'createLocalMediaStream'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:148:26 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:148:26 Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch:
fun <T, R> T.let(block: (T) -> R): R
[R|Contract description]
<
CallsInPlace(block, EXACTLY_ONCE)
>
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:148:30 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:149:26 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:149:26 Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch:
fun <T, R> T.let(block: (T) -> R): R
[R|Contract description]
<
CallsInPlace(block, EXACTLY_ONCE)
>
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:149:30 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:151:17 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:151:21 Cannot infer type for this parameter. Please specify it explicitly.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:151:39 Unresolved reference 'addStream'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:156:47 Unresolved reference 'VideoSource'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:157:32 Unresolved reference 'Camera2Enumerator'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:159:47 Unresolved reference 'isFrontFacing'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:159:80 Unresolved reference 'it'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:161:47 Unresolved reference 'isBackFacing'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:161:79 Unresolved reference 'it'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:165:36 Unresolved reference 'SurfaceTextureHelper'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:165:80 Unresolved reference 'EglBase'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:166:83 Unresolved reference 'CameraVideoCapturer'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:168:28 Unresolved reference 'initialize'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:168:83 Unresolved reference 'capturerObserver'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:169:28 Unresolved reference 'startCapture'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:174:27 Unresolved reference 'MediaConstraints'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:175:25 Unresolved reference 'createOffer'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:175:46 Unresolved reference 'SdpObserver'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:176:13 'onCreateSuccess' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:176:48 Unresolved reference 'SessionDescription'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:177:33 Unresolved reference 'setLocalDescription'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:177:62 Unresolved reference 'SdpObserver'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:178:21 'onSetSuccess' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:182:45 Unresolved reference 'description'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:186:21 'onSetFailure' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:187:21 'onCreateSuccess' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:187:54 Unresolved reference 'SessionDescription'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:188:21 'onCreateFailure' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:191:13 'onCreateFailure' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:192:13 'onSetSuccess' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:193:13 'onSetFailure' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:198:20 Unresolved reference 'SessionDescription'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:198:39 Unresolved reference 'SessionDescription'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:199:25 Unresolved reference 'setRemoteDescription'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:199:55 Unresolved reference 'SdpObserver'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:200:13 'onSetSuccess' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:203:13 'onSetFailure' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:204:13 'onCreateSuccess' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:204:46 Unresolved reference 'SessionDescription'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:205:13 'onCreateFailure' overrides nothing.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:210:28 Unresolved reference 'IceCandidate'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:211:25 Unresolved reference 'addIceCandidate'.
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:216:32 Unresolved reference 'dispose'.
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:compileDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
> Compilation error. See log for more details
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
BUILD FAILED in 2s
31 actionable tasks: 13 executed, 18 from cache

6
build_output.log Normal file
View File

@@ -0,0 +1,6 @@
ERROR: JAVA_HOME is set to an invalid directory: /usr/lib/jvm/java-11-openjdk-amd64
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation.

350
godeye_server.py Normal file
View File

@@ -0,0 +1,350 @@
#!/usr/bin/env python3
"""
Простой веб-сервер для панели оператора GodEye
Обслуживает WebRTC интерфейс и Socket.IO соединения
"""
import socketio
import eventlet
import eventlet.wsgi
from flask import Flask, send_file
import json
import uuid
from datetime import datetime
import os
# Создаем Flask приложение
app = Flask(__name__)
# Создаем Socket.IO сервер
sio = socketio.Server(cors_allowed_origins="*")
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
# Хранилище подключенных устройств и операторов
connected_devices = {}
connected_operators = {}
active_sessions = {}
@app.route('/')
def serve_interface():
"""Обслуживает панель оператора"""
return send_file('operator-interface.html')
@sio.event
def connect(sid, environ):
"""Обработка подключения клиента"""
print(f"[{datetime.now().strftime('%H:%M:%S')}] 🔗 Клиент подключился: {sid}")
@sio.event
def disconnect(sid):
"""Обработка отключения клиента"""
print(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ Клиент отключился: {sid}")
# Удаляем из устройств
if sid in connected_devices:
device_data = connected_devices[sid]
del connected_devices[sid]
sio.emit('device_disconnected', {
'deviceId': device_data['deviceId'],
'deviceName': device_data['deviceName']
})
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📱 Устройство отключилось: {device_data['deviceName']}")
# Удаляем из операторов
if sid in connected_operators:
del connected_operators[sid]
print(f"[{datetime.now().strftime('%H:%M:%S')}] 👨‍💼 Оператор отключился")
@sio.event
def device_register(sid, data):
"""Регистрация Android устройства"""
device_id = data.get('deviceId')
device_name = data.get('deviceName', 'Unknown Device')
connected_devices[sid] = {
'deviceId': device_id,
'deviceName': device_name,
'connectedAt': datetime.now().isoformat()
}
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📱 Устройство зарегистрировано: {device_name} ({device_id})")
# Уведомляем всех операторов о новом устройстве
sio.emit('device_connected', {
'deviceId': device_id,
'deviceName': device_name
})
# Отправляем подтверждение устройству
sio.emit('registration_confirmed', {
'status': 'success',
'message': 'Устройство успешно зарегистрировано'
}, room=sid)
@sio.event
def operator_register(sid, data):
"""Регистрация оператора"""
operator_id = data.get('operatorId', f'operator_{uuid.uuid4().hex[:8]}')
connected_operators[sid] = {
'operatorId': operator_id,
'connectedAt': datetime.now().isoformat()
}
print(f"[{datetime.now().strftime('%H:%M:%S')}] 👨‍💼 Оператор зарегистрирован: {operator_id}")
# Отправляем список подключенных устройств
devices_list = []
for device_sid, device_data in connected_devices.items():
devices_list.append({
'deviceId': device_data['deviceId'],
'deviceName': device_data['deviceName']
})
sio.emit('devices_list', {'devices': devices_list}, room=sid)
@sio.event
def request_camera(sid, data):
"""Запрос камеры от оператора"""
device_id = data.get('deviceId')
operator_id = data.get('operatorId')
camera_type = data.get('cameraType', 'back') # Убеждаемся что есть значение по умолчанию
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📹 Запрос камеры от {operator_id} для устройства {device_id}, тип камеры: {camera_type}")
# Находим устройство по ID
target_device_sid = None
for device_sid, device_data in connected_devices.items():
if device_data['deviceId'] == device_id:
target_device_sid = device_sid
break
if target_device_sid:
session_id = str(uuid.uuid4())
# Сохраняем сессию с правильными данными
active_sessions[session_id] = {
'deviceSid': target_device_sid,
'operatorSid': sid,
'deviceId': device_id,
'operatorId': operator_id,
'cameraType': camera_type, # Явно сохраняем тип камеры
'status': 'pending', # Добавляем статус
'startedAt': datetime.now().isoformat()
}
# Отправляем запрос устройству с полными данными
sio.emit('camera_request', {
'sessionId': session_id,
'operatorId': operator_id,
'cameraType': camera_type,
'deviceId': device_id, # Добавляем deviceId
'message': f'Оператор {operator_id} запрашивает доступ к камере'
}, room=target_device_sid)
# Уведомляем оператора о создании сессии
sio.emit('session_created', {
'sessionId': session_id,
'deviceId': device_id,
'cameraType': camera_type,
'status': 'pending'
}, room=sid)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📤 Запрос отправлен устройству, сессия: {session_id}, камера: {camera_type}")
else:
sio.emit('error', {
'message': f'Устройство {device_id} не найдено'
}, room=sid)
@sio.event
def camera_approved(sid, data):
"""Подтверждение доступа к камере от устройства"""
session_id = data.get('sessionId')
if session_id in active_sessions:
session = active_sessions[session_id]
operator_sid = session['operatorSid']
# Обновляем статус сессии
active_sessions[session_id]['status'] = 'approved'
print(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ Доступ к камере подтвержден для сессии: {session_id}")
# Уведомляем оператора с полными данными
sio.emit('camera_approved', {
'sessionId': session_id,
'deviceId': session['deviceId'],
'cameraType': session['cameraType'],
'status': 'approved'
}, room=operator_sid)
@sio.event
def camera_rejected(sid, data):
"""Отклонение доступа к камере от устройства"""
session_id = data.get('sessionId')
reason = data.get('reason', 'Отклонено пользователем')
if session_id in active_sessions:
session = active_sessions[session_id]
operator_sid = session['operatorSid']
print(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ Доступ к камере отклонен для сессии: {session_id}, причина: {reason}")
# Уведомляем оператора об отклонении
sio.emit('camera_rejected', {
'sessionId': session_id,
'deviceId': session['deviceId'],
'reason': reason
}, room=operator_sid)
# Удаляем сессию, так как она отклонена
del active_sessions[session_id]
@sio.event
def camera_started(sid, data):
"""Уведомление о запуске камеры"""
session_id = data.get('sessionId')
if session_id in active_sessions:
session = active_sessions[session_id]
operator_sid = session['operatorSid']
# Обновляем статус сессии на активный
active_sessions[session_id]['status'] = 'active'
active_sessions[session_id]['actualStartedAt'] = datetime.now().isoformat()
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📹 Камера запущена для сессии: {session_id}")
# Уведомляем оператора с обновленным статусом
sio.emit('camera_started', {
'sessionId': session_id,
'deviceId': session['deviceId'],
'cameraType': session['cameraType'],
'status': 'active'
}, room=operator_sid)
# Также отправляем обновление статуса сессии
sio.emit('session_status_update', {
'sessionId': session_id,
'status': 'active',
'cameraType': session['cameraType']
}, room=operator_sid)
@sio.event
def webrtc_offer(sid, data):
"""Пересылка WebRTC offer от устройства к оператору"""
session_id = data.get('sessionId')
offer = data.get('offer')
if session_id in active_sessions:
session = active_sessions[session_id]
operator_sid = session['operatorSid']
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📡 Пересылка WebRTC offer для сессии: {session_id}")
# Пересылаем offer оператору
sio.emit('webrtc_offer', {
'sessionId': session_id,
'offer': offer
}, room=operator_sid)
@sio.event
def webrtc_answer(sid, data):
"""Пересылка WebRTC answer от оператора к устройству"""
session_id = data.get('sessionId')
answer = data.get('answer')
if session_id in active_sessions:
session = active_sessions[session_id]
device_sid = session['deviceSid']
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📡 Пересылка WebRTC answer для сессии: {session_id}")
# Пересылаем answer устройству
sio.emit('webrtc_answer', {
'sessionId': session_id,
'answer': answer
}, room=device_sid)
@sio.event
def webrtc_ice_candidate(sid, data):
"""Пересылка ICE candidates между устройством и оператором"""
session_id = data.get('sessionId')
candidate = data.get('candidate')
if session_id in active_sessions:
session = active_sessions[session_id]
# Определяем куда пересылать (от устройства к оператору или наоборот)
if sid == session['deviceSid']:
# От устройства к оператору
target_sid = session['operatorSid']
else:
# От оператора к устройству
target_sid = session['deviceSid']
print(f"[{datetime.now().strftime('%H:%M:%S')}] 🧊 Пересылка ICE candidate для сессии: {session_id}")
sio.emit('webrtc_ice_candidate', {
'sessionId': session_id,
'candidate': candidate
}, room=target_sid)
@sio.event
def switch_camera(sid, data):
"""Переключение камеры"""
session_id = data.get('sessionId')
if session_id in active_sessions:
session = active_sessions[session_id]
device_sid = session['deviceSid']
print(f"[{datetime.now().strftime('%H:%M:%S')}] 🔄 Переключение камеры для сессии: {session_id}")
# Отправляем команду устройству
sio.emit('switch_camera', {
'sessionId': session_id
}, room=device_sid)
@sio.event
def stop_camera(sid, data):
"""Остановка камеры"""
session_id = data.get('sessionId')
if session_id in active_sessions:
session = active_sessions[session_id]
device_sid = session['deviceSid']
operator_sid = session['operatorSid']
print(f"[{datetime.now().strftime('%H:%M:%S')}] 🛑 Остановка камеры для сессии: {session_id}")
# Отправляем команду устройству
sio.emit('stop_camera', {
'sessionId': session_id
}, room=device_sid)
# Уведомляем оператора
sio.emit('camera_stopped', {
'sessionId': session_id
}, room=operator_sid)
# Удаляем сессию
del active_sessions[session_id]
if __name__ == '__main__':
# ИСПРАВЛЕНО: Используем localhost для локального тестирования
host = '0.0.0.0' # Слушаем на всех интерфейсах
port = 3001
print("🚀 Запуск GodEye сервера...")
print(f"🌐 Веб-интерфейс: http://localhost:{port}")
print(f"🔌 Socket.IO endpoint: http://localhost:{port}/socket.io/")
print("📱 Настройте Android приложение на адрес вашего компьютера")
print("-" * 50)
try:
eventlet.wsgi.server(eventlet.listen((host, port)), app)
except KeyboardInterrupt:
print("\n🛑 Сервер остановлен")
except Exception as e:
print(f"❌ Ошибка сервера: {e}")

View File

@@ -7,6 +7,10 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# ?????????? Java 17 ??? ??????????? ??????????
org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
@@ -20,4 +24,7 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true
# Enable Compose Compiler
android.enableJetifier=true

View File

@@ -1,13 +1,22 @@
[versions]
agp = "8.13.0"
kotlin = "2.0.21"
kotlin = "2.0.20"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.4"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.11.0"
composeBom = "2024.09.00"
composeBom = "2025.09.01"
appcompat = "1.7.1"
material = "1.12.0"
constraintlayout = "2.2.0"
preference = "1.2.1"
coroutines = "1.9.0"
okhttp = "4.12.0"
socketio = "2.1.1"
gson = "2.10.1"
webrtc = "1.0.32006"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -17,16 +26,34 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-preference = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference" }
# Coroutines
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
# Network
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
socketio-client = { group = "io.socket", name = "socket.io-client", version.ref = "socketio" }
# JSON
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
# WebRTC - используем Stream WebRTC библиотеку которая доступна
webrtc-android = { group = "io.getstream", name = "stream-webrtc-android", version = "1.0.7" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View File

@@ -3,11 +3,28 @@
# Автоматическая сборка, установка APK на LG G6 и мониторинг логов
# Использование: ./install_to_lg_g6.sh
# Принудительно используем Java 17 - ту же версию что в gradle.properties
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
export GRADLE_OPTS="-Dorg.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64 -Dorg.gradle.jvmargs=-Xmx2048m"
export PATH="/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH"
# Проверяем что Java 17 доступна
if [ ! -d "$JAVA_HOME" ]; then
echo "❌ Java 17 не найдена в $JAVA_HOME"
echo "Проверим доступные версии Java:"
ls -la /usr/lib/jvm/
exit 1
fi
echo "🔧 GodEye APK Builder & Installer для LG G6"
echo "=============================================="
echo "🔧 JAVA_HOME: $JAVA_HOME"
echo "🔧 Java версия: $($JAVA_HOME/bin/java -version 2>&1 | head -1)"
APK_PATH="/home/trevor/AndroidStudioProjects/GodEye/app/build/outputs/apk/debug/app-debug.apk"
LG_G6_DEVICE="LGMG600S9b4da66b"
APP_PACKAGE="com.example.godeye"
MAIN_ACTIVITY=".LegacyMainActivity"
# Функция для логирования с временными метками
log() {
@@ -33,7 +50,7 @@ log "✅ LG G6 найден: $LG_G6_DEVICE"
# Собираем проект
log "🔨 Собираю проект..."
if ! ./gradlew assembleDebug; then
if ! JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 ./gradlew assembleDebug --no-daemon; then
echo "❌ Ошибка сборки проекта!"
exit 1
fi
@@ -61,7 +78,7 @@ adb -s "$LG_G6_DEVICE" logcat -c
# Запускаем приложение
log "🚀 Запускаю приложение GodEye..."
adb -s "$LG_G6_DEVICE" shell am start -n com.example.godeye/.MainActivity
adb -s "$LG_G6_DEVICE" shell am start -n ${APP_PACKAGE}/${MAIN_ACTIVITY}
# Ждем немного для инициализации
sleep 2

4
logs/signaling.log Normal file
View File

@@ -0,0 +1,4 @@
Traceback (most recent call last):
File "/home/trevor/AndroidStudioProjects/GodEye/signaling_server.py", line 8, in <module>
import websockets
ModuleNotFoundError: No module named 'websockets'

219
monitor_network.sh Executable file
View File

@@ -0,0 +1,219 @@
#!/bin/bash
# GodEye Network Traffic Monitor
# Скрипт для отслеживания сетевого трафика Android приложения
echo "🔍 GodEye Network Traffic Monitor"
echo "=================================="
# Исправленная проверка подключения устройства
# Проверяем все подключенные устройства
echo "📱 Поиск устройств Android..."
DEVICES=($(adb devices | grep -w "device" | awk '{print $1}'))
if [ ${#DEVICES[@]} -eq 0 ]; then
echo "❌ Устройства Android не найдены!"
echo "Убедитесь что:"
echo "1. Устройство подключено по USB"
echo "2. Включена отладка по USB"
echo "3. Разрешена отладка для этого компьютера"
exit 1
elif [ ${#DEVICES[@]} -eq 1 ]; then
DEVICE=${DEVICES[0]}
echo "📱 Найдено устройство: $DEVICE"
else
echo "📱 Найдено несколько устройств:"
for i in "${!DEVICES[@]}"; do
echo "$((i+1))) ${DEVICES[$i]}"
done
# Проверяем есть ли LG G6
LG_G6="LGMG600S9b4da66b"
if [[ " ${DEVICES[@]} " =~ " ${LG_G6} " ]]; then
DEVICE=$LG_G6
echo "🎯 Автоматически выбран LG G6: $DEVICE"
else
read -p "Выберите устройство (1-${#DEVICES[@]}): " choice
if [[ $choice -ge 1 && $choice -le ${#DEVICES[@]} ]]; then
DEVICE=${DEVICES[$((choice-1))]}
echo "✅ Выбрано устройство: $DEVICE"
else
echo "❌ Неверный выбор!"
exit 1
fi
fi
fi
# Функция для выполнения adb команд с указанием конкретного устройства
adb_cmd() {
adb -s "$DEVICE" "$@"
}
# Функция для отслеживания логов приложения
monitor_app_logs() {
echo ""
echo "📋 Мониторинг логов приложения GodEye..."
echo "Фильтр: WebRTC, Network, Connection"
echo "Нажмите Ctrl+C для остановки"
echo "----------------------------------------"
adb_cmd logcat -c # Очищаем логи
adb_cmd logcat | grep -E "(WEBRTC|WebRTC|GodEye|Network|Connection)" --color=always
}
# Функция для отслеживания сетевых соединений
monitor_network_connections() {
echo ""
echo "🌐 Мониторинг сетевых соединений..."
echo "Поиск активных UDP/TCP соединений от GodEye"
echo "----------------------------------------"
# Получаем PID приложения GodEye
APP_PID=$(adb_cmd shell ps | grep com.example.godeye | awk '{print $2}' | head -1)
if [ -z "$APP_PID" ]; then
echo "❌ Приложение GodEye не запущено!"
return 1
fi
echo "🎯 PID приложения GodEye: $APP_PID"
echo ""
while true; do
echo "$(date '+%H:%M:%S') - Активные соединения:"
adb_cmd shell netstat | grep $APP_PID | while read line; do
echo " 📡 $line"
done
echo ""
sleep 5
done
}
# Функция для анализа трафика через tcpdump (требует root)
monitor_packet_capture() {
echo ""
echo "📦 Захват пакетов (требует root)..."
echo "Попытка захвата UDP трафика на порты WebRTC"
echo "----------------------------------------"
# Проверяем root доступ
ROOT_CHECK=$(adb_cmd shell su -c "echo test" 2>/dev/null)
if [ "$ROOT_CHECK" != "test" ]; then
echo "❌ Root доступ недоступен. Пропускаем захват пакетов."
return 1
fi
echo "✅ Root доступ получен"
echo "Захватываем UDP трафик (порты 1024-65535)..."
# Захватываем UDP пакеты
adb_cmd shell su -c "tcpdump -i any -n udp and portrange 1024-65535" | \
while read line; do
# Фильтруем только интересные пакеты
if echo "$line" | grep -E "(STUN|RTP|DTLS)" > /dev/null; then
echo "🚀 WebRTC: $line"
elif echo "$line" | grep -E "([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)" > /dev/null; then
echo "📡 UDP: $line"
fi
done
}
# Функция для мониторинга использования сети приложением
monitor_data_usage() {
echo ""
echo "📊 Мониторинг использования данных..."
echo "Отслеживание трафика приложения GodEye"
echo "----------------------------------------"
# Получаем UID приложения (исправленная переменная)
local APP_UID=$(adb_cmd shell dumpsys package com.example.godeye | grep "userId=" | head -1 | sed 's/.*userId=\([0-9]*\).*/\1/')
if [ -z "$APP_UID" ]; then
echo "❌ Не удалось получить UID приложения!"
echo "Проверяем установленные пакеты..."
adb_cmd shell pm list packages | grep godeye
return 1
fi
echo "🆔 UID приложения: $APP_UID"
echo ""
# Мониторим использование сети
while true; do
echo "$(date '+%H:%M:%S') - Статистика сети:"
# Получаем статистику из /proc/net/xt_qtaguid/stats
STATS=$(adb_cmd shell cat /proc/net/xt_qtaguid/stats 2>/dev/null | grep " $APP_UID ")
if [ ! -z "$STATS" ]; then
echo "$STATS" | while read line; do
# Парсим статистику
RX_BYTES=$(echo $line | awk '{print $6}')
TX_BYTES=$(echo $line | awk '{print $8}')
if [ "$RX_BYTES" -gt 0 ] || [ "$TX_BYTES" -gt 0 ]; then
echo " 📥 Получено: $(format_bytes $RX_BYTES)"
echo " 📤 Отправлено: $(format_bytes $TX_BYTES)"
fi
done
else
echo " Сетевая активность не обнаружена"
fi
echo ""
sleep 3
done
}
# Функция форматирования байтов
format_bytes() {
local bytes=$1
if [ $bytes -lt 1024 ]; then
echo "${bytes} B"
elif [ $bytes -lt $((1024*1024)) ]; then
echo "$((bytes/1024)) KB"
elif [ $bytes -lt $((1024*1024*1024)) ]; then
echo "$((bytes/1024/1024)) MB"
else
echo "$((bytes/1024/1024/1024)) GB"
fi
}
# Главное меню
echo ""
echo "Выберите режим мониторинга:"
echo "1) 📋 Логи приложения (WebRTC/Network)"
echo "2) 🌐 Сетевые соединения"
echo "3) 📦 Захват пакетов (root)"
echo "4) 📊 Использование данных"
echo "5) 🔄 Все режимы одновременно"
echo ""
read -p "Введите номер (1-5): " choice
case $choice in
1)
monitor_app_logs
;;
2)
monitor_network_connections
;;
3)
monitor_packet_capture
;;
4)
monitor_data_usage
;;
5)
echo "🚀 Запуск всех мониторов..."
echo "Откройте дополнительные терминалы для других режимов"
monitor_app_logs &
monitor_network_connections &
monitor_data_usage &
wait
;;
*)
echo "❌ Неверный выбор!"
exit 1
;;
esac

533
operator-interface.html Normal file
View File

@@ -0,0 +1,533 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GodEye - Интерфейс Оператора</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: #1a1a1a;
color: #fff;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.status {
padding: 15px;
border-radius: 8px;
margin: 10px 0;
font-weight: bold;
}
.status.disconnected { background: #d32f2f; }
.status.connecting { background: #f57c00; }
.status.connected { background: #388e3c; }
.video-container {
position: relative;
background: #000;
border-radius: 8px;
overflow: hidden;
margin: 20px 0;
}
#remoteVideo {
width: 100%;
height: 480px;
object-fit: cover;
}
.controls {
display: flex;
gap: 10px;
margin: 20px 0;
flex-wrap: wrap;
}
button {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s;
}
.btn-primary { background: #2196f3; color: white; }
.btn-success { background: #4caf50; color: white; }
.btn-danger { background: #f44336; color: white; }
.btn-warning { background: #ff9800; color: white; }
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.device-list {
background: #2a2a2a;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.device-item {
background: #3a3a3a;
padding: 15px;
border-radius: 6px;
margin: 10px 0;
cursor: pointer;
transition: background 0.3s;
}
.device-item:hover {
background: #4a4a4a;
}
.logs {
background: #0a0a0a;
border-radius: 8px;
padding: 15px;
height: 200px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
margin: 20px 0;
}
.log-entry {
margin: 2px 0;
padding: 2px 0;
}
.log-info { color: #81c784; }
.log-warning { color: #ffb74d; }
.log-error { color: #e57373; }
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-card {
background: #2a2a2a;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #2196f3;
}
.network-monitor {
background: #2a2a2a;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.traffic-indicator {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0;
}
.traffic-bar {
flex: 1;
height: 8px;
background: #1a1a1a;
border-radius: 4px;
overflow: hidden;
}
.traffic-fill {
height: 100%;
transition: width 0.3s;
}
.upload { background: #4caf50; }
.download { background: #2196f3; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🕵️ GodEye - Интерфейс Оператора</h1>
<div id="connectionStatus" class="status disconnected">
🔴 Не подключен к серверу сигналинга
</div>
</div>
<div class="controls">
<button id="connectBtn" class="btn-primary" onclick="connectToSignaling()">
📡 Подключиться к серверу
</button>
<button id="refreshBtn" class="btn-warning" onclick="refreshDevices()">
🔄 Обновить устройства
</button>
<button id="disconnectBtn" class="btn-danger" onclick="disconnect()" disabled>
🔌 Отключиться
</button>
</div>
<div class="device-list">
<h3>📱 Доступные устройства</h3>
<div id="deviceList">
<p>Нет доступных устройств. Убедитесь, что приложение запущено на устройстве.</p>
</div>
</div>
<div class="video-container">
<video id="remoteVideo" autoplay playsinline muted>
<p>Видео поток будет отображен здесь после подключения к устройству</p>
</video>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="connectionTime">--:--</div>
<div>Время подключения</div>
</div>
<div class="stat-card">
<div class="stat-value" id="videoQuality">---</div>
<div>Качество видео</div>
</div>
<div class="stat-card">
<div class="stat-value" id="latency">--- ms</div>
<div>Задержка</div>
</div>
<div class="stat-card">
<div class="stat-value" id="bandwidth">--- kbps</div>
<div>Пропускная способность</div>
</div>
</div>
<div class="network-monitor">
<h3>📊 Мониторинг сети</h3>
<div class="traffic-indicator">
<span>📤 Исходящий:</span>
<div class="traffic-bar">
<div id="uploadBar" class="traffic-fill upload" style="width: 0%"></div>
</div>
<span id="uploadSpeed">0 KB/s</span>
</div>
<div class="traffic-indicator">
<span>📥 Входящий:</span>
<div class="traffic-bar">
<div id="downloadBar" class="traffic-fill download" style="width: 0%"></div>
</div>
<span id="downloadSpeed">0 KB/s</span>
</div>
</div>
<div class="logs">
<div id="logContainer"></div>
</div>
</div>
<script>
// Глобальные переменные
let ws = null;
let pc = null;
let currentSession = null;
let connectionStartTime = null;
let statsInterval = null;
// WebRTC конфигурация
const pcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
// Функции логирования
function log(message, type = 'info') {
const logContainer = document.getElementById('logContainer');
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
console.log(message);
}
// Подключение к серверу сигналинга
function connectToSignaling() {
if (ws && ws.readyState === WebSocket.OPEN) {
log('Уже подключен к серверу сигналинга', 'warning');
return;
}
log('Подключение к серверу сигналинга...', 'info');
updateConnectionStatus('connecting', '🟡 Подключение к серверу...');
ws = new WebSocket('ws://localhost:8765');
ws.onopen = function() {
log('✅ Подключен к серверу сигналинга', 'info');
updateConnectionStatus('connected', '🟢 Подключен к серверу сигналинга');
document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false;
refreshDevices();
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
handleSignalingMessage(data);
};
ws.onclose = function() {
log('❌ Соединение с сервером разорвано', 'error');
updateConnectionStatus('disconnected', '🔴 Не подключен к серверу');
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
};
ws.onerror = function(error) {
log('❌ Ошибка подключения к серверу: ' + error, 'error');
updateConnectionStatus('disconnected', '🔴 Ошибка подключения');
};
}
// Обработка сообщений сигналинга
function handleSignalingMessage(data) {
log(`📨 Получено сообщение: ${data.type}`, 'info');
switch(data.type) {
case 'client_registered':
log(`🆔 Зарегистрирован как клиент: ${data.client_id}`, 'info');
break;
case 'session_joined':
currentSession = data.session_id;
log(`✅ Подключен к сессии: ${data.session_id}`, 'info');
log(`📱 Устройство: ${data.device_info.model || 'Unknown'}`, 'info');
connectionStartTime = Date.now();
startStatsMonitoring();
break;
case 'offer':
handleOffer(data.sdp);
break;
case 'ice_candidate':
handleIceCandidate(data.candidate);
break;
case 'hangup':
handleHangup();
break;
case 'error':
log(`❌ Ошибка: ${data.message}`, 'error');
break;
}
}
// Обработка SDP offer
async function handleOffer(offer) {
try {
log('📞 Получен SDP Offer, создание PeerConnection...', 'info');
pc = new RTCPeerConnection(pcConfig);
pc.onicecandidate = function(event) {
if (event.candidate) {
log('🧊 Отправка ICE candidate', 'info');
sendMessage({
type: 'ice_candidate',
session_id: currentSession,
candidate: event.candidate
});
}
};
pc.ontrack = function(event) {
log('📹 Получен видео поток', 'info');
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = event.streams[0];
};
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
log('📞 Отправка SDP Answer', 'info');
sendMessage({
type: 'answer',
session_id: currentSession,
sdp: answer
});
} catch (error) {
log(`❌ Ошибка обработки offer: ${error}`, 'error');
}
}
// Обработка ICE candidate
async function handleIceCandidate(candidate) {
try {
if (pc) {
await pc.addIceCandidate(candidate);
log('🧊 ICE candidate добавлен', 'info');
}
} catch (error) {
log(`❌ Ошибка добавления ICE candidate: ${error}`, 'error');
}
}
// Подключение к устройству
function connectToDevice(sessionId) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
log('❌ Нет подключения к серверу сигналинга', 'error');
return;
}
log(`🔗 Подключение к устройству в сессии: ${sessionId}`, 'info');
sendMessage({
type: 'join_session',
session_id: sessionId
});
}
// Отправка сообщения через WebSocket
function sendMessage(message) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
}
// Обновление статуса подключения
function updateConnectionStatus(status, text) {
const statusElement = document.getElementById('connectionStatus');
statusElement.className = `status ${status}`;
statusElement.textContent = text;
}
// Обновление списка устройств
function refreshDevices() {
const deviceList = document.getElementById('deviceList');
deviceList.innerHTML = '<p>🔍 Поиск доступных устройств...</p>';
// Имитация поиска устройств (в реальности будет запрос к серверу)
setTimeout(() => {
deviceList.innerHTML = `
<div class="device-item" onclick="connectToDevice('demo-session-1')">
<strong>📱 LG G6 (LGMG600S9b4da66b)</strong><br>
<small>Android 8.0 • IP: 192.168.1.100 • Последняя активность: только что</small>
</div>
`;
}, 1000);
}
// Завершение соединения
function disconnect() {
if (pc) {
pc.close();
pc = null;
}
if (currentSession) {
sendMessage({
type: 'hangup',
session_id: currentSession
});
currentSession = null;
}
if (ws) {
ws.close();
ws = null;
}
if (statsInterval) {
clearInterval(statsInterval);
statsInterval = null;
}
connectionStartTime = null;
updateConnectionStatus('disconnected', '🔴 Отключен');
document.getElementById('remoteVideo').srcObject = null;
log('🔌 Соединение разорвано', 'info');
}
// Обработка завершения сессии
function handleHangup() {
log('📴 Сессия завершена', 'info');
disconnect();
}
// Мониторинг статистики
function startStatsMonitoring() {
if (statsInterval) {
clearInterval(statsInterval);
}
statsInterval = setInterval(updateStats, 1000);
}
function updateStats() {
// Обновление времени подключения
if (connectionStartTime) {
const elapsed = Math.floor((Date.now() - connectionStartTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
document.getElementById('connectionTime').textContent =
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
// WebRTC статистика (упрощенная)
if (pc) {
pc.getStats().then(stats => {
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
const fps = report.framesPerSecond || 0;
const bitrate = Math.round((report.bytesReceived || 0) * 8 / 1000);
document.getElementById('videoQuality').textContent = `${fps} FPS`;
document.getElementById('bandwidth').textContent = `${bitrate} kbps`;
}
});
});
}
}
// Автоподключение при загрузке страницы
window.onload = function() {
log('🚀 GodEye Operator Interface загружен', 'info');
// Попытка автоподключения через 1 секунду
setTimeout(() => {
log('🔄 Попытка автоподключения к серверу...', 'info');
connectToSignaling();
}, 1000);
};
// Обработка закрытия окна
window.onbeforeunload = function() {
disconnect();
};
</script>
</body>
</html>

269
signaling_server.py Normal file
View File

@@ -0,0 +1,269 @@
#!/usr/bin/env python3
"""
GodEye Signaling Server
Сервер для обмена SDP и ICE кандидатами между устройством и оператором
"""
import asyncio
import websockets
import json
import logging
from datetime import datetime
import uuid
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SignalingServer:
def __init__(self):
self.clients = {} # client_id -> websocket
self.sessions = {} # session_id -> {device: ws, operator: ws}
async def register_client(self, websocket, path):
"""Регистрация нового клиента"""
client_id = str(uuid.uuid4())
self.clients[client_id] = websocket
logger.info(f"🔗 Клиент подключен: {client_id}")
try:
# Отправляем ID клиенту
await websocket.send(json.dumps({
"type": "client_registered",
"client_id": client_id,
"timestamp": datetime.now().isoformat()
}))
async for message in websocket:
await self.handle_message(client_id, message)
except websockets.exceptions.ConnectionClosed:
logger.info(f"🔌 Клиент отключен: {client_id}")
finally:
# Очистка при отключении
await self.cleanup_client(client_id)
async def handle_message(self, client_id, message):
"""Обработка сообщений от клиентов"""
try:
data = json.loads(message)
message_type = data.get("type")
logger.info(f"📨 Сообщение от {client_id}: {message_type}")
if message_type == "create_session":
await self.create_session(client_id, data)
elif message_type == "join_session":
await self.join_session(client_id, data)
elif message_type == "offer":
await self.relay_offer(client_id, data)
elif message_type == "answer":
await self.relay_answer(client_id, data)
elif message_type == "ice_candidate":
await self.relay_ice_candidate(client_id, data)
elif message_type == "hangup":
await self.handle_hangup(client_id, data)
else:
logger.warning(f"⚠️ Неизвестный тип сообщения: {message_type}")
except json.JSONDecodeError:
logger.error(f"❌ Ошибка парсинга JSON от {client_id}")
except Exception as e:
logger.error(f"❌ Ошибка обработки сообщения: {e}")
async def create_session(self, client_id, data):
"""Создание новой сессии (устройство)"""
session_id = str(uuid.uuid4())
device_info = data.get("device_info", {})
self.sessions[session_id] = {
"device": self.clients[client_id],
"operator": None,
"device_info": device_info,
"created_at": datetime.now().isoformat()
}
# Отправляем подтверждение устройству
await self.clients[client_id].send(json.dumps({
"type": "session_created",
"session_id": session_id,
"timestamp": datetime.now().isoformat()
}))
logger.info(f"📱 Сессия создана: {session_id} для устройства {device_info.get('model', 'Unknown')}")
async def join_session(self, client_id, data):
"""Подключение к сессии (оператор)"""
session_id = data.get("session_id")
if session_id not in self.sessions:
await self.clients[client_id].send(json.dumps({
"type": "error",
"message": "Session not found"
}))
return
session = self.sessions[session_id]
if session["operator"] is not None:
await self.clients[client_id].send(json.dumps({
"type": "error",
"message": "Session already has an operator"
}))
return
# Подключаем оператора
session["operator"] = self.clients[client_id]
# Уведомляем оператора
await self.clients[client_id].send(json.dumps({
"type": "session_joined",
"session_id": session_id,
"device_info": session["device_info"],
"timestamp": datetime.now().isoformat()
}))
# Уведомляем устройство
await session["device"].send(json.dumps({
"type": "operator_joined",
"session_id": session_id,
"timestamp": datetime.now().isoformat()
}))
logger.info(f"👤 Оператор подключен к сессии: {session_id}")
async def relay_offer(self, client_id, data):
"""Передача SDP offer"""
session_id = data.get("session_id")
session = self.sessions.get(session_id)
if not session or session["operator"] is None:
return
# Передаем offer оператору
await session["operator"].send(json.dumps({
"type": "offer",
"session_id": session_id,
"sdp": data.get("sdp"),
"timestamp": datetime.now().isoformat()
}))
logger.info(f"📞 SDP Offer передан в сессии: {session_id}")
async def relay_answer(self, client_id, data):
"""Передача SDP answer"""
session_id = data.get("session_id")
session = self.sessions.get(session_id)
if not session:
return
# Передаем answer устройству
await session["device"].send(json.dumps({
"type": "answer",
"session_id": session_id,
"sdp": data.get("sdp"),
"timestamp": datetime.now().isoformat()
}))
logger.info(f"📞 SDP Answer передан в сессии: {session_id}")
async def relay_ice_candidate(self, client_id, data):
"""Передача ICE кандидатов"""
session_id = data.get("session_id")
session = self.sessions.get(session_id)
if not session:
return
# Определяем кому передавать (устройству или оператору)
if session["device"] == self.clients[client_id]:
# От устройства к оператору
if session["operator"]:
await session["operator"].send(json.dumps({
"type": "ice_candidate",
"session_id": session_id,
"candidate": data.get("candidate"),
"timestamp": datetime.now().isoformat()
}))
else:
# От оператора к устройству
await session["device"].send(json.dumps({
"type": "ice_candidate",
"session_id": session_id,
"candidate": data.get("candidate"),
"timestamp": datetime.now().isoformat()
}))
logger.info(f"🧊 ICE candidate передан в сессии: {session_id}")
async def handle_hangup(self, client_id, data):
"""Завершение сессии"""
session_id = data.get("session_id")
session = self.sessions.get(session_id)
if not session:
return
# Уведомляем обе стороны
if session["device"]:
await session["device"].send(json.dumps({
"type": "hangup",
"session_id": session_id,
"timestamp": datetime.now().isoformat()
}))
if session["operator"]:
await session["operator"].send(json.dumps({
"type": "hangup",
"session_id": session_id,
"timestamp": datetime.now().isoformat()
}))
# Удаляем сессию
del self.sessions[session_id]
logger.info(f"📴 Сессия завершена: {session_id}")
async def cleanup_client(self, client_id):
"""Очистка при отключении клиента"""
if client_id in self.clients:
del self.clients[client_id]
# Удаляем сессии с этим клиентом
sessions_to_remove = []
for session_id, session in self.sessions.items():
if (session["device"] == self.clients.get(client_id) or
session["operator"] == self.clients.get(client_id)):
sessions_to_remove.append(session_id)
for session_id in sessions_to_remove:
del self.sessions[session_id]
logger.info(f"🧹 Сессия {session_id} удалена из-за отключения клиента")
def main():
"""Запуск сигналинг сервера"""
server = SignalingServer()
print("🚀 GodEye Signaling Server")
print("==========================")
print("📡 Сервер запущен на ws://localhost:8765")
print("💡 Для остановки нажмите Ctrl+C")
print()
# Исправляем проблему с event loop
async def run_server():
start_server = websockets.serve(
server.register_client,
"0.0.0.0", # Слушаем на всех интерфейсах
8765
)
await start_server
await asyncio.Future() # Запускаем вечно
try:
asyncio.run(run_server())
except KeyboardInterrupt:
print("\n🛑 Сервер остановлен")
if __name__ == "__main__":
main()

76
simple_test_server.py Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""
Простой тест для проверки автоподтверждения
Имитирует сервер Socket.IO и отправляет запрос камеры
"""
import socket
import json
import time
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
class TestServer:
def __init__(self, host='192.168.219.108', port=3002):
self.host = host
self.port = port
self.clients = []
def start(self):
print(f"🚀 Starting test server on {self.host}:{self.port}")
print("📱 Configure Android app to connect to this server")
print("🔄 Waiting for Android app connection...")
# Простой TCP сервер для имитации Socket.IO
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
server_socket.bind((self.host, self.port))
server_socket.listen(5)
while True:
client_socket, addr = server_socket.accept()
print(f"📱 Android device connected from {addr}")
# Имитируем ответ Socket.IO
response = "HTTP/1.1 200 OK\r\n"
response += "Content-Type: application/json\r\n"
response += "Access-Control-Allow-Origin: *\r\n\r\n"
response += '{"status":"connected"}'
client_socket.send(response.encode())
# Отправляем запрос камеры через некоторое время
time.sleep(3)
self.send_camera_request(client_socket)
client_socket.close()
except KeyboardInterrupt:
print("\n🛑 Stopping test server")
finally:
server_socket.close()
def send_camera_request(self, client_socket):
camera_request = {
"event": "camera-request",
"data": {
"sessionId": f"test_session_{int(time.time())}",
"operatorId": "test_operator",
"cameraType": "back",
"timestamp": int(time.time() * 1000)
}
}
print(f"📹 Sending camera request: {camera_request}")
try:
message = json.dumps(camera_request) + "\n"
client_socket.send(message.encode())
print("✅ Camera request sent to Android app")
except Exception as e:
print(f"❌ Failed to send camera request: {e}")
if __name__ == "__main__":
test_server = TestServer()
test_server.start()

227
start_godeye.sh Executable file
View File

@@ -0,0 +1,227 @@
#!/bin/bash
# GodEye Complete Setup & Launch Script
# Скрипт для запуска полной инфраструктуры GodEye
echo "🚀 GodEye Complete Infrastructure Setup"
echo "======================================="
# Функция для получения IP адреса
get_local_ip() {
local ip=$(ip route get 1.1.1.1 | awk '{print $7}' | head -1)
if [ -z "$ip" ]; then
ip=$(hostname -I | awk '{print $1}')
fi
echo "$ip"
}
# Получаем локальный IP
LOCAL_IP=$(get_local_ip)
echo "🌐 Локальный IP адрес: $LOCAL_IP"
# Проверяем зависимости
echo ""
echo "🔍 Проверка зависимостей..."
# Проверяем Python
if ! command -v python3 &> /dev/null; then
echo "❌ Python3 не найден! Установите Python3"
exit 1
fi
# Проверяем websockets
if ! python3 -c "import websockets" 2>/dev/null; then
echo "📦 Установка websockets..."
pip3 install websockets
fi
# Проверяем adb
if ! command -v adb &> /dev/null; then
echo "❌ ADB не найден! Установите Android SDK Platform Tools"
echo "sudo apt install android-tools-adb android-tools-fastboot"
exit 1
fi
echo "✅ Все зависимости установлены"
# Функция для остановки всех процессов
cleanup() {
echo ""
echo "🛑 Остановка всех сервисов..."
# Остановка сигналинг сервера
if [ ! -z "$SIGNALING_PID" ]; then
kill $SIGNALING_PID 2>/dev/null
echo "📡 Сигналинг сервер остановлен"
fi
# Остановка веб сервера
if [ ! -z "$WEB_PID" ]; then
kill $WEB_PID 2>/dev/null
echo "🌐 Веб сервер остановлен"
fi
echo "👋 Система остановлена"
exit 0
}
# Обработка сигнала прерывания
trap cleanup SIGINT SIGTERM
# Создаем директории для логов
mkdir -p logs
echo ""
echo "🔧 Запуск сервисов..."
# 1. Запуск сигналинг сервера
echo "📡 Запуск сигналинг сервера на порту 8765..."
python3 signaling_server.py > logs/signaling.log 2>&1 &
SIGNALING_PID=$!
# Ждем запуска сигналинг сервера
sleep 2
# Проверяем что сигналинг сервер запустился
if ps -p $SIGNALING_PID > /dev/null; then
echo "✅ Сигналинг сервер запущен (PID: $SIGNALING_PID)"
else
echo "❌ Ошибка запуска сигналинг сервера"
cat logs/signaling.log
exit 1
fi
# 2. Запуск простого веб сервера для интерфейса оператора
echo "🌐 Запуск веб сервера на порту 8080..."
python3 -m http.server 8080 > logs/webserver.log 2>&1 &
WEB_PID=$!
# Ждем запуска веб сервера
sleep 1
if ps -p $WEB_PID > /dev/null; then
echo "Веб сервер запущен (PID: $WEB_PID)"
else
echo "❌ Ошибка запуска веб сервера"
exit 1
fi
# 3. Обновляем IP адрес в веб интерфейсе
echo "🔧 Обновление IP адреса в интерфейсе оператора..."
sed -i "s/localhost:8765/${LOCAL_IP}:8765/g" operator-interface.html
# 4. Проверяем подключение Android устройства
echo ""
echo "📱 Проверка подключения Android устройства..."
DEVICES=($(adb devices | grep -w "device" | awk '{print $1}'))
if [ ${#DEVICES[@]} -eq 0 ]; then
echo "⚠️ Android устройства не найдены!"
echo "Подключите устройство и включите отладку по USB"
else
echo "✅ Найдено устройств: ${#DEVICES[@]}"
for device in "${DEVICES[@]}"; do
echo " 📲 $device"
done
fi
# 5. Сборка и установка APK (если устройство подключено)
if [ ${#DEVICES[@]} -gt 0 ]; then
echo ""
echo "🔨 Сборка и установка приложения..."
# Обновляем IP адрес в Android приложении
SIGNALING_CLIENT_FILE="app/src/main/java/com/example/godeye/network/SignalingClient.kt"
if [ -f "$SIGNALING_CLIENT_FILE" ]; then
sed -i "s/192.168.1.100/${LOCAL_IP}/g" "$SIGNALING_CLIENT_FILE"
echo "🔧 IP адрес обновлен в Android приложении: $LOCAL_IP"
fi
# Собираем проект
echo "⚙️ Сборка проекта..."
./gradlew assembleDebug > logs/build.log 2>&1
if [ $? -eq 0 ]; then
echo "✅ Проект собран успешно"
# Устанавливаем на все подключенные устройства
for device in "${DEVICES[@]}"; do
echo "📲 Установка на устройство: $device"
adb -s "$device" install -r app/build/outputs/apk/debug/app-debug.apk
if [ $? -eq 0 ]; then
echo "✅ Приложение установлено на $device"
# Запускаем приложение
echo "🚀 Запуск приложения на $device"
adb -s "$device" shell am start -n com.example.godeye/.MainActivity
else
echo "❌ Ошибка установки на $device"
fi
done
else
echo "❌ Ошибка сборки проекта"
echo "Проверьте logs/build.log для подробностей"
fi
fi
# 6. Выводим информацию для подключения
echo ""
echo "🎯 Система запущена и готова к работе!"
echo "======================================"
echo ""
echo "📡 Сигналинг сервер: ws://${LOCAL_IP}:8765"
echo "🌐 Интерфейс оператора: http://${LOCAL_IP}:8080/operator-interface.html"
echo "📱 Android приложение: установлено и запущено на устройствах"
echo ""
echo "🔗 Инструкции по подключению:"
echo "1. Откройте браузер и перейдите по адресу: http://${LOCAL_IP}:8080/operator-interface.html"
echo "2. В браузере нажмите 'Подключиться к серверу'"
echo "3. На Android устройстве запустите приложение GodEye"
echo "4. В приложении нажмите 'Начать трансляцию'"
echo "5. В браузере должно появиться устройство в списке доступных"
echo "6. Нажмите на устройство для подключения"
echo ""
echo "📊 Мониторинг:"
echo "• Логи сигналинга: tail -f logs/signaling.log"
echo "• Логи веб сервера: tail -f logs/webserver.log"
echo "• Логи сборки: tail -f logs/build.log"
echo ""
echo "🛑 Для остановки нажмите Ctrl+C"
echo ""
# Основной цикл мониторинга
echo "🔄 Мониторинг системы (каждые 10 сек)..."
while true; do
sleep 10
# Проверяем состояние сервисов
SIGNALING_STATUS="❌"
WEB_STATUS="❌"
if ps -p $SIGNALING_PID > /dev/null 2>&1; then
SIGNALING_STATUS="✅"
fi
if ps -p $WEB_PID > /dev/null 2>&1; then
WEB_STATUS="✅"
fi
# Проверяем подключенные устройства
DEVICE_COUNT=$(adb devices | grep -w "device" | wc -l)
echo "$(date '+%H:%M:%S') - Статус: Сигналинг $SIGNALING_STATUS | Веб $WEB_STATUS | Устройств: $DEVICE_COUNT"
# Если какой-то сервис упал, пытаемся перезапустить
if ! ps -p $SIGNALING_PID > /dev/null 2>&1; then
echo "⚠️ Сигналинг сервер упал, перезапуск..."
python3 signaling_server.py > logs/signaling.log 2>&1 &
SIGNALING_PID=$!
fi
if ! ps -p $WEB_PID > /dev/null 2>&1; then
echo "⚠️ Веб сервер упал, перезапуск..."
python3 -m http.server 8080 > logs/webserver.log 2>&1 &
WEB_PID=$!
fi
done

43
test_camera_request.py Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import socketio
import time
import json
# Создаем Socket.IO клиент
sio = socketio.SimpleClient()
def test_camera_request():
try:
# Подключаемся к серверу (используем правильный IP из тестов)
print("Connecting to server...")
sio.connect('http://192.168.219.108:3001')
print("Connected!")
# Ждем немного для стабилизации соединения
time.sleep(2)
# Отправляем запрос камеры
camera_request = {
"sessionId": f"test_session_{int(time.time())}",
"operatorId": "test_operator",
"cameraType": "back",
"timestamp": int(time.time() * 1000)
}
print(f"Sending camera request: {camera_request}")
sio.emit('camera-request', camera_request)
# Ждем ответа
print("Waiting for response...")
time.sleep(10)
except Exception as e:
print(f"Error: {e}")
finally:
try:
sio.disconnect()
except:
pass
if __name__ == "__main__":
test_camera_request()

247
venv/bin/Activate.ps1 Normal file
View File

@@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

70
venv/bin/activate Normal file
View File

@@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /home/trevor/AndroidStudioProjects/GodEye/venv)
else
# use the path as-is
export VIRTUAL_ENV=/home/trevor/AndroidStudioProjects/GodEye/venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

27
venv/bin/activate.csh Normal file
View File

@@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/trevor/AndroidStudioProjects/GodEye/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(venv) '
endif
alias pydoc python -m pydoc
rehash

69
venv/bin/activate.fish Normal file
View File

@@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/trevor/AndroidStudioProjects/GodEye/venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(venv) '
end

8
venv/bin/flask Executable file
View File

@@ -0,0 +1,8 @@
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from flask.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip Executable file
View File

@@ -0,0 +1,8 @@
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3 Executable file
View File

@@ -0,0 +1,8 @@
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3.12 Executable file
View File

@@ -0,0 +1,8 @@
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

1
venv/bin/python Symbolic link
View File

@@ -0,0 +1 @@
python3

1
venv/bin/python3 Symbolic link
View File

@@ -0,0 +1 @@
/usr/bin/python3

1
venv/bin/python3.12 Symbolic link
View File

@@ -0,0 +1 @@
python3

8
venv/bin/websockets Executable file
View File

@@ -0,0 +1,8 @@
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from websockets.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@@ -0,0 +1,164 @@
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
/* Greenlet object interface */
#ifndef Py_GREENLETOBJECT_H
#define Py_GREENLETOBJECT_H
#include <Python.h>
#ifdef __cplusplus
extern "C" {
#endif
/* This is deprecated and undocumented. It does not change. */
#define GREENLET_VERSION "1.0.0"
#ifndef GREENLET_MODULE
#define implementation_ptr_t void*
#endif
typedef struct _greenlet {
PyObject_HEAD
PyObject* weakreflist;
PyObject* dict;
implementation_ptr_t pimpl;
} PyGreenlet;
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
/* C API functions */
/* Total number of symbols that are exported */
#define PyGreenlet_API_pointers 12
#define PyGreenlet_Type_NUM 0
#define PyExc_GreenletError_NUM 1
#define PyExc_GreenletExit_NUM 2
#define PyGreenlet_New_NUM 3
#define PyGreenlet_GetCurrent_NUM 4
#define PyGreenlet_Throw_NUM 5
#define PyGreenlet_Switch_NUM 6
#define PyGreenlet_SetParent_NUM 7
#define PyGreenlet_MAIN_NUM 8
#define PyGreenlet_STARTED_NUM 9
#define PyGreenlet_ACTIVE_NUM 10
#define PyGreenlet_GET_PARENT_NUM 11
#ifndef GREENLET_MODULE
/* This section is used by modules that uses the greenlet C API */
static void** _PyGreenlet_API = NULL;
# define PyGreenlet_Type \
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
# define PyExc_GreenletError \
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
# define PyExc_GreenletExit \
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
/*
* PyGreenlet_New(PyObject *args)
*
* greenlet.greenlet(run, parent=None)
*/
# define PyGreenlet_New \
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
_PyGreenlet_API[PyGreenlet_New_NUM])
/*
* PyGreenlet_GetCurrent(void)
*
* greenlet.getcurrent()
*/
# define PyGreenlet_GetCurrent \
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
/*
* PyGreenlet_Throw(
* PyGreenlet *greenlet,
* PyObject *typ,
* PyObject *val,
* PyObject *tb)
*
* g.throw(...)
*/
# define PyGreenlet_Throw \
(*(PyObject * (*)(PyGreenlet * self, \
PyObject * typ, \
PyObject * val, \
PyObject * tb)) \
_PyGreenlet_API[PyGreenlet_Throw_NUM])
/*
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
*
* g.switch(*args, **kwargs)
*/
# define PyGreenlet_Switch \
(*(PyObject * \
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
_PyGreenlet_API[PyGreenlet_Switch_NUM])
/*
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
*
* g.parent = new_parent
*/
# define PyGreenlet_SetParent \
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
/*
* PyGreenlet_GetParent(PyObject* greenlet)
*
* return greenlet.parent;
*
* This could return NULL even if there is no exception active.
* If it does not return NULL, you are responsible for decrementing the
* reference count.
*/
# define PyGreenlet_GetParent \
(*(PyGreenlet* (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
/*
* deprecated, undocumented alias.
*/
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
# define PyGreenlet_MAIN \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
# define PyGreenlet_STARTED \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
# define PyGreenlet_ACTIVE \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
/* Macro that imports greenlet and initializes C API */
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
keep the older definition to be sure older code that might have a copy of
the header still works. */
# define PyGreenlet_Import() \
{ \
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
}
#endif /* GREENLET_MODULE */
#ifdef __cplusplus
}
#endif
#endif /* !Py_GREENLETOBJECT_H */

View File

@@ -0,0 +1 @@
pip

View File

@@ -0,0 +1,376 @@
Mozilla Public License Version 2.0
==================================
Copyright 2009-2024 Joshua Bronson. All rights reserved.
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

Some files were not shown because too many files have changed in this diff Show More