Compare commits

..

2 Commits

Author SHA1 Message Date
79256cd9fc main commit 2025-10-06 09:40:51 +09:00
b1de55d253 init commit 2025-10-04 11:55:35 +09:00
3635 changed files with 374895 additions and 188597 deletions

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

26
.idea/appInsightsSettings.xml generated Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

6
.idea/copilot.data.migration.agent.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.ask.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.edit.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

18
.idea/deploymentTargetSelector.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-10-05T21:09:42.524082016Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=LGMG600S9b4da66b" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -0,0 +1,50 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>

10
.idea/migrations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

16
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,16 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<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>

17
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

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

@@ -0,0 +1,79 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.godeye"
compileSdk = 29 // Android 10 для максимальной совместимости
defaultConfig {
applicationId = "com.example.godeye.legacy"
minSdk = 24
targetSdk = 28 // Android 9
versionCode = 1
versionName = "1.0-legacy"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
sourceSets {
getByName("main") {
manifest.srcFile("src/main/AndroidManifest-legacy.xml")
}
}
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"
}
// ТОЛЬКО ViewBinding для legacy версии
buildFeatures {
compose = false
viewBinding = true
}
}
dependencies {
// МИНИМАЛЬНЫЕ зависимости для Android 9
implementation("androidx.core:core-ktx:1.3.2")
implementation("androidx.appcompat:appcompat:1.2.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.2.0")
// UI компоненты для legacy
implementation("com.google.android.material:material:1.3.0")
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.activity:activity-ktx:1.1.0")
// ViewModel для legacy
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0")
// Сетевые библиотеки
implementation("io.socket:socket.io-client:2.1.0")
implementation("com.google.code.gson:gson:2.8.9")
// Корутины
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.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

@@ -27,22 +27,28 @@ android {
) )
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "11"
} }
buildFeatures { buildFeatures {
compose = true compose = true
viewBinding = true viewBinding = true
} }
// Исправляем проблему с Java toolchain composeOptions {
java { kotlinCompilerExtensionVersion = "1.5.15"
toolchain { }
languageVersion.set(JavaLanguageVersion.of(17))
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
} }
@@ -51,55 +57,44 @@ dependencies {
// Core Android // Core Android
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.preference)
// Compose
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui) implementation(libs.androidx.ui)
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.material3)
// ViewModel and LiveData // Material Icons
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") implementation("androidx.compose.material:material-icons-core")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.activity:activity-ktx:1.8.2")
// Socket.IO для WebSocket соединения // Coroutines
implementation("io.socket:socket.io-client:2.1.2") implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
// Пока уберем WebRTC зависимость - создадим заглушку для демонстрации // Network
// В реальном проекте нужно будет настроить правильную WebRTC библиотеку implementation(libs.okhttp)
implementation(libs.okhttp.logging)
implementation(libs.socketio.client)
// Camera2 API // JSON
implementation("androidx.camera:camera-core:1.3.1") implementation(libs.gson)
implementation("androidx.camera:camera-camera2:1.3.1")
implementation("androidx.camera:camera-lifecycle:1.3.1")
implementation("androidx.camera:camera-view:1.3.1")
// JSON парсинг // WebRTC
implementation("com.google.code.gson:gson:2.10.1") implementation(libs.webrtc.android)
// Корутины
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// RecyclerView
implementation("androidx.recyclerview:recyclerview:1.3.2")
// Work Manager для фоновых задач
implementation("androidx.work:work-runtime-ktx:2.9.0")
// Permissions
implementation("androidx.activity:activity-compose:1.8.2")
// Navigation
implementation("androidx.navigation:navigation-compose:2.7.6")
// Testing // Testing
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest) 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

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Разрешения для Android 9 -->
<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="28">
<!-- ТОЛЬКО LegacyMainActivity как точка входа -->
<activity
android:name=".LegacyMainActivity"
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>
<!-- LegacyCameraActivity -->
<activity
android:name=".LegacyCameraActivity"
android:exported="false"
android:theme="@style/Theme.GodEye"
android:screenOrientation="portrait"
android:launchMode="singleTop" />
<!-- SocketService для legacy -->
<service
android:name=".services.SocketService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

View File

@@ -2,56 +2,85 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<!-- Основные разрешения --> <!-- Разрешения для сигналлинга и WebRTC -->
<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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <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.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Требования к оборудованию --> <!-- Разрешения для камеры и микрофона -->
<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" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.microphone" android:required="true" /> <uses-feature android:name="android.hardware.microphone" android:required="true" />
<application <application
android:name=".GodEyeApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.GodEye"> android:theme="@style/Theme.GodEye"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="35">
<!-- MainActivity с Compose интерфейсом -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GodEye" android:theme="@style/Theme.GodEye"
android:screenOrientation="portrait"> android:screenOrientation="portrait"
android:launchMode="singleTop">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Socket Service для WebSocket соединения --> <!-- Legacy активности для совместимости -->
<activity
android:name=".LegacyMainActivity"
android:exported="false"
android:theme="@style/Theme.GodEye"
android:screenOrientation="portrait"
android:launchMode="singleTop" />
<activity
android:name=".LegacyCameraActivity"
android:exported="false"
android:theme="@style/Theme.GodEye"
android:screenOrientation="portrait"
android:launchMode="singleTop" />
<!-- Сервисы -->
<service <service
android:name=".services.SocketService" android:name=".services.SocketService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false" />
android:foregroundServiceType="camera" />
<!-- Camera Service для работы с камерой -->
<service <service
android:name=".services.CameraService" android:name=".services.SignalingService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="camera" /> android:foregroundServiceType="dataSync" />
<service
android:name=".services.ConnectionService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application> </application>
</manifest> </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

@@ -0,0 +1,94 @@
package com.example.godeye
import android.app.Application
import com.example.godeye.utils.ErrorHandler
import com.example.godeye.utils.Logger
/**
* GodEyeApplication - главный класс приложения для инициализации глобальных компонентов
* Соответствует требованиям ТЗ для правильной инициализации приложения
*/
class GodEyeApplication : Application() {
private val errorHandler = ErrorHandler()
override fun onCreate() {
super.onCreate()
Logger.step("APPLICATION_START", "GodEye Application starting...")
try {
// Инициализация глобальных компонентов
initializeLogging()
setupExceptionHandler()
Logger.step("APPLICATION_READY", "GodEye Application initialized successfully")
} catch (e: Exception) {
Logger.error("APPLICATION_INIT_ERROR", "Failed to initialize application", e)
}
}
/**
* Инициализация системы логирования
*/
private fun initializeLogging() {
Logger.step("LOGGING_INIT", "Initializing logging system")
// Система логирования уже инициализирована в Logger object
Logger.d("Application context available: ${this.javaClass.simpleName}")
}
/**
* Настройка глобального обработчика исключений
*/
private fun setupExceptionHandler() {
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
try {
// Используем наш ErrorHandler для обработки исключений
errorHandler.handleUncaughtException(thread, exception)
// Специальная обработка известных безопасных ошибок
when {
// Compose hover events bug - игнорируем
exception is IllegalStateException &&
exception.message?.contains("ACTION_HOVER_EXIT event was not cleared") == true -> {
Logger.d("Ignoring Compose hover event bug")
return@setDefaultUncaughtExceptionHandler
}
// Ошибки при завершении приложения - игнорируем
exception is InternalError &&
exception.message?.contains("Thread starting during runtime shutdown") == true -> {
Logger.d("Ignoring shutdown thread creation error")
return@setDefaultUncaughtExceptionHandler
}
// WebRTC ошибки - логируем но не крашим
exception.message?.contains("Failed to set local") == true -> {
Logger.error("WEBRTC_ERROR", "WebRTC error handled gracefully", exception)
return@setDefaultUncaughtExceptionHandler
}
// Для критических ошибок используем стандартный обработчик
else -> {
Logger.error("CRITICAL_ERROR", "Critical error, delegating to default handler", exception)
defaultHandler?.uncaughtException(thread, exception)
}
}
} catch (handlerException: Exception) {
// Если наш обработчик тоже упал, используем стандартный
Logger.error("HANDLER_ERROR", "Error in exception handler", handlerException)
defaultHandler?.uncaughtException(thread, exception)
}
}
Logger.step("EXCEPTION_HANDLER_SET", "Global exception handler configured")
}
override fun onTerminate() {
Logger.step("APPLICATION_TERMINATE", "GodEye Application terminating...")
super.onTerminate()
}
}

View File

@@ -0,0 +1,292 @@
package com.example.godeye
import android.Manifest
import android.content.pm.PackageManager
import android.hardware.Camera
import android.os.Bundle
import android.view.SurfaceHolder
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.example.godeye.databinding.ActivityLegacyCameraBinding
import com.example.godeye.utils.Logger
import java.io.IOException
/**
* LegacyCameraActivity - камера для Android 9
* Использует устаревший Camera API для максимальной совместимости
*/
@Suppress("DEPRECATION")
class LegacyCameraActivity : AppCompatActivity(), SurfaceHolder.Callback {
private lateinit var binding: ActivityLegacyCameraBinding
private var camera: Camera? = null
private var surfaceHolder: SurfaceHolder? = null
private var isPreviewRunning = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
Logger.step("LEGACY_CAMERA_CREATE", "Creating LegacyCameraActivity for Android 9")
binding = ActivityLegacyCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
setupCamera()
Logger.step("LEGACY_CAMERA_CREATE_SUCCESS", "LegacyCameraActivity created successfully")
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_CREATE_ERROR", "Error creating LegacyCameraActivity", e)
Toast.makeText(this, "Ошибка инициализации камеры", Toast.LENGTH_LONG).show()
finish()
}
}
private fun setupUI() {
binding.apply {
// Настройка кнопок
btnBack.setOnClickListener {
finish()
}
btnCapture.setOnClickListener {
capturePhoto()
}
btnSwitchCamera.setOnClickListener {
switchCamera()
}
// Настройка информации
tvCameraInfo.text = "📹 Legacy Camera для Android 9"
}
}
private fun setupCamera() {
try {
if (!checkCameraPermission()) {
Toast.makeText(this, "Нет разрешения на использование камеры", Toast.LENGTH_LONG).show()
finish()
return
}
// Настройка SurfaceView для предварительного просмотра
surfaceHolder = binding.surfaceViewCamera.holder
surfaceHolder?.addCallback(this)
surfaceHolder?.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
Logger.step("LEGACY_CAMERA_SETUP", "Camera surface setup completed")
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_SETUP_ERROR", "Error setting up camera", e)
Toast.makeText(this, "Ошибка настройки камеры", Toast.LENGTH_SHORT).show()
}
}
private fun checkCameraPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
}
override fun surfaceCreated(holder: SurfaceHolder) {
try {
Logger.step("LEGACY_SURFACE_CREATED", "Camera surface created")
startCamera()
} catch (e: Exception) {
Logger.error("LEGACY_SURFACE_CREATE_ERROR", "Error on surface created", e)
}
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
try {
Logger.step("LEGACY_SURFACE_CHANGED", "Camera surface changed: ${width}x${height}")
if (isPreviewRunning) {
camera?.stopPreview()
}
startCameraPreview()
} catch (e: Exception) {
Logger.error("LEGACY_SURFACE_CHANGE_ERROR", "Error on surface changed", e)
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
try {
Logger.step("LEGACY_SURFACE_DESTROYED", "Camera surface destroyed")
stopCamera()
} catch (e: Exception) {
Logger.error("LEGACY_SURFACE_DESTROY_ERROR", "Error on surface destroyed", e)
}
}
private fun startCamera() {
try {
if (camera == null) {
camera = Camera.open()
Logger.step("LEGACY_CAMERA_OPENED", "Legacy camera opened successfully")
}
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_OPEN_ERROR", "Error opening camera", e)
Toast.makeText(this, "Не удалось открыть камеру", Toast.LENGTH_SHORT).show()
}
}
private fun startCameraPreview() {
try {
camera?.let { cam ->
cam.setPreviewDisplay(surfaceHolder)
// Настройка параметров камеры для Android 9
val parameters = cam.parameters
val supportedSizes = parameters.supportedPreviewSizes
// Выбираем подходящий размер превью
supportedSizes?.let { sizes ->
val optimalSize = getOptimalPreviewSize(sizes, binding.surfaceViewCamera.width, binding.surfaceViewCamera.height)
optimalSize?.let {
parameters.setPreviewSize(it.width, it.height)
cam.parameters = parameters
}
}
cam.startPreview()
isPreviewRunning = true
// Обновляем UI
binding.tvStatus.text = "✅ Камера активна"
Logger.step("LEGACY_CAMERA_PREVIEW_STARTED", "Camera preview started successfully")
}
} catch (e: IOException) {
Logger.error("LEGACY_CAMERA_PREVIEW_ERROR", "Error starting camera preview", e)
Toast.makeText(this, "Ошибка запуска предварительного просмотра", Toast.LENGTH_SHORT).show()
}
}
private fun getOptimalPreviewSize(sizes: List<Camera.Size>, width: Int, height: Int): Camera.Size? {
val targetRatio = width.toDouble() / height
var optimalSize: Camera.Size? = null
var minDiff = Double.MAX_VALUE
for (size in sizes) {
val ratio = size.width.toDouble() / size.height
if (Math.abs(ratio - targetRatio) > 0.1) continue
if (Math.abs(size.height - height) < minDiff) {
optimalSize = size
minDiff = Math.abs(size.height - height).toDouble()
}
}
if (optimalSize == null) {
minDiff = Double.MAX_VALUE
for (size in sizes) {
if (Math.abs(size.height - height) < minDiff) {
optimalSize = size
minDiff = Math.abs(size.height - height).toDouble()
}
}
}
return optimalSize
}
private fun stopCamera() {
try {
camera?.let { cam ->
if (isPreviewRunning) {
cam.stopPreview()
isPreviewRunning = false
}
cam.release()
camera = null
binding.tvStatus.text = "⚪ Камера остановлена"
Logger.step("LEGACY_CAMERA_STOPPED", "Camera stopped and released")
}
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_STOP_ERROR", "Error stopping camera", e)
}
}
private fun capturePhoto() {
try {
if (!isPreviewRunning) {
Toast.makeText(this, "Камера не активна", Toast.LENGTH_SHORT).show()
return
}
Logger.step("LEGACY_CAMERA_CAPTURE", "Capturing photo with legacy camera")
// Простая реализация захвата фото
camera?.takePicture(null, null) { data, _ ->
try {
Logger.step("LEGACY_PHOTO_CAPTURED", "Photo captured, size: ${data.size} bytes")
Toast.makeText(this@LegacyCameraActivity, "Фото сделано!", Toast.LENGTH_SHORT).show()
// Здесь можно добавить сохранение фото или отправку на сервер
} catch (e: Exception) {
Logger.error("LEGACY_PHOTO_SAVE_ERROR", "Error processing captured photo", e)
}
}
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_CAPTURE_ERROR", "Error capturing photo", e)
Toast.makeText(this, "Ошибка съемки фото", Toast.LENGTH_SHORT).show()
}
}
private fun switchCamera() {
try {
Logger.step("LEGACY_CAMERA_SWITCH", "Attempting to switch camera")
// Для Android 9 просто показываем сообщение
Toast.makeText(this, "Переключение камеры (в разработке)", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_SWITCH_ERROR", "Error switching camera", e)
}
}
override fun onPause() {
super.onPause()
try {
if (isPreviewRunning) {
camera?.stopPreview()
isPreviewRunning = false
}
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_PAUSE_ERROR", "Error pausing camera", e)
}
}
override fun onResume() {
super.onResume()
try {
if (camera != null && !isPreviewRunning) {
startCameraPreview()
}
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_RESUME_ERROR", "Error resuming camera", e)
}
}
override fun onDestroy() {
super.onDestroy()
try {
stopCamera()
Logger.step("LEGACY_CAMERA_DESTROY", "LegacyCameraActivity destroyed safely")
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_DESTROY_ERROR", "Error destroying camera activity", e)
}
}
}

View File

@@ -0,0 +1,245 @@
package com.example.godeye
import android.Manifest
import android.content.ComponentName
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
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.godeye.databinding.ActivityLegacyMainBinding
import com.example.godeye.services.SocketService
import com.example.godeye.utils.Logger
/**
* LegacyMainActivity - упрощенная версия для Android 9
* Использует классические Android Views вместо Compose
* Максимальная совместимость с Android 9
*/
class LegacyMainActivity : AppCompatActivity() {
private lateinit var binding: ActivityLegacyMainBinding
private var socketService: SocketService? = null
private var isServiceBound = false
// Подключение к SocketService
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Logger.step("LEGACY_SERVICE_CONNECTED", "SocketService connected to LegacyMainActivity")
val binder = service as? SocketService.LocalBinder
socketService = binder?.getService()
isServiceBound = true
updateUI()
}
override fun onServiceDisconnected(name: ComponentName?) {
Logger.step("LEGACY_SERVICE_DISCONNECTED", "SocketService disconnected")
socketService = null
isServiceBound = false
updateUI()
}
}
// Обработка разрешений
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.values.all { it }
if (allGranted) {
Logger.step("LEGACY_PERMISSIONS_GRANTED", "All permissions granted")
updateUI()
} else {
Logger.step("LEGACY_PERMISSIONS_DENIED", "Some permissions denied")
Toast.makeText(this, "Требуются разрешения для работы приложения", Toast.LENGTH_LONG).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
Logger.step("LEGACY_ACTIVITY_CREATE", "LegacyMainActivity onCreate for Android 9")
binding = ActivityLegacyMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
checkPermissions()
startAndBindService()
Logger.step("LEGACY_ACTIVITY_CREATE_SUCCESS", "LegacyMainActivity created successfully")
} catch (e: Exception) {
Logger.error("LEGACY_ACTIVITY_CREATE_ERROR", "Error creating LegacyMainActivity", e)
Toast.makeText(this, "Ошибка запуска приложения", Toast.LENGTH_LONG).show()
}
}
private fun setupUI() {
binding.apply {
// Настройка кнопок
btnConnect.setOnClickListener {
connectToServer()
}
btnDisconnect.setOnClickListener {
disconnectFromServer()
}
btnCamera.setOnClickListener {
openCamera()
}
btnSettings.setOnClickListener {
openSettings()
}
// Установка начального состояния
updateUI()
}
}
private fun checkPermissions() {
val requiredPermissions = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE
)
val missingPermissions = requiredPermissions.filter { permission ->
ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED
}
if (missingPermissions.isNotEmpty()) {
Logger.step("LEGACY_REQUEST_PERMISSIONS", "Requesting permissions: ${missingPermissions.joinToString()}")
permissionLauncher.launch(missingPermissions.toTypedArray())
} else {
Logger.step("LEGACY_PERMISSIONS_OK", "All permissions already granted")
}
}
private fun startAndBindService() {
try {
val intent = Intent(this, SocketService::class.java)
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) {
Logger.error("LEGACY_SERVICE_BIND_ERROR", "Error binding to service", e)
}
}
private fun updateUI() {
binding.apply {
// Обновление статуса подключения
if (isServiceBound && socketService != null) {
tvStatus.text = "✅ Сервис подключен"
btnConnect.isEnabled = true
btnCamera.isEnabled = hasAllPermissions()
} else {
tvStatus.text = "❌ Сервис не подключен"
btnConnect.isEnabled = false
btnCamera.isEnabled = false
}
// Обновление информации об устройстве
val deviceInfo = "📱 ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}\n" +
"🤖 Android ${android.os.Build.VERSION.RELEASE}"
tvDeviceInfo.text = deviceInfo
// Обновление статуса разрешений
val permissionsStatus = if (hasAllPermissions()) {
"✅ Разрешения предоставлены"
} else {
"⚠️ Требуются разрешения"
}
tvPermissions.text = permissionsStatus
}
}
private fun hasAllPermissions(): Boolean {
val requiredPermissions = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE
)
return requiredPermissions.all { permission ->
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
}
private fun connectToServer() {
try {
Logger.step("LEGACY_CONNECT_SERVER", "Attempting to connect to server")
// Простая заглушка для подключения
binding.tvConnectionStatus.text = "🔄 Подключение к серверу..."
Toast.makeText(this, "Подключение к серверу...", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Logger.error("LEGACY_CONNECT_ERROR", "Error connecting to server", e)
binding.tvConnectionStatus.text = "❌ Ошибка подключения"
}
}
private fun disconnectFromServer() {
try {
Logger.step("LEGACY_DISCONNECT_SERVER", "Disconnecting from server")
binding.tvConnectionStatus.text = "⚪ Отключено"
Toast.makeText(this, "Отключено от сервера", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Logger.error("LEGACY_DISCONNECT_ERROR", "Error disconnecting", e)
}
}
private fun openCamera() {
if (!hasAllPermissions()) {
Toast.makeText(this, "Требуются разрешения камеры", Toast.LENGTH_SHORT).show()
checkPermissions()
return
}
try {
Logger.step("LEGACY_OPEN_CAMERA", "Opening legacy camera")
val intent = Intent(this, LegacyCameraActivity::class.java)
startActivity(intent)
} catch (e: Exception) {
Logger.error("LEGACY_CAMERA_ERROR", "Error opening camera", e)
Toast.makeText(this, "Ошибка открытия камеры", Toast.LENGTH_SHORT).show()
}
}
private fun openSettings() {
try {
Logger.step("LEGACY_OPEN_SETTINGS", "Opening settings")
Toast.makeText(this, "Настройки (в разработке)", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Logger.error("LEGACY_SETTINGS_ERROR", "Error opening settings", e)
}
}
override fun onDestroy() {
super.onDestroy()
try {
if (isServiceBound) {
unbindService(serviceConnection)
isServiceBound = false
}
Logger.step("LEGACY_ACTIVITY_DESTROY", "LegacyMainActivity destroyed safely")
} catch (e: Exception) {
Logger.error("LEGACY_DESTROY_ERROR", "Error destroying activity", e)
}
}
}

View File

@@ -1,118 +1,176 @@
package com.example.godeye package com.example.godeye
import android.Manifest import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.*
import androidx.activity.viewModels import androidx.compose.material.icons.Icons
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.filled.*
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.Scaffold import androidx.compose.runtime.*
import androidx.compose.material3.SnackbarHost import androidx.compose.ui.Alignment
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat import androidx.compose.ui.text.font.FontWeight
import com.example.godeye.managers.PermissionManager import androidx.compose.ui.unit.dp
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.ConnectionService
import com.example.godeye.ui.components.*
import com.example.godeye.ui.screens.MainScreen 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.ui.theme.GodEyeTheme
import com.example.godeye.ui.viewmodels.MainViewModel import com.example.godeye.utils.PermissionHelper
import com.example.godeye.utils.Logger import com.example.godeye.utils.PreferenceManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels() // Убираем SignalingManager - используем только ConnectionManager + AutoApprovalManager
private lateinit var permissionManager: PermissionManager // private lateinit var signalingManager: SignalingManager
private lateinit var connectionManager: ConnectionManager
// Launcher для запроса разрешений private lateinit var autoApprovalManager: AutoApprovalManager
private val permissionsLauncher = registerForActivityResult( private lateinit var permissionHelper: PermissionHelper
ActivityResultContracts.RequestMultiplePermissions() private lateinit var preferenceManager: PreferenceManager
) { permissions ->
val allGranted = permissions.values.all { it }
if (allGranted) {
Logger.d("All permissions granted")
viewModel.startServices() // Запуск сервисов после получения разрешений
} else {
Logger.w("Some permissions were denied")
val deniedPermissions = permissions.filterValues { !it }.keys
Logger.w("Denied permissions: ${deniedPermissions.joinToString(", ")}")
}
// Логируем статус разрешений
permissionManager.logPermissionsStatus()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Logger.d("MainActivity created") // Инициализируем менеджеры (БЕЗ SignalingManager)
permissionManager = PermissionManager(this) preferenceManager = PreferenceManager(this)
// signalingManager = SignalingManager(this) // УБИРАЕМ
// Проверяем разрешения при запуске connectionManager = ConnectionManager(this, preferenceManager)
checkAndRequestPermissions() autoApprovalManager = AutoApprovalManager(this, preferenceManager, connectionManager)
if (permissionManager.hasAllRequiredPermissions()) { permissionHelper = PermissionHelper(this)
viewModel.startServices() // Запуск сервисов если разрешения уже есть
}
setContent { setContent {
GodEyeTheme { GodEyeTheme {
val snackbarHostState = remember { SnackbarHostState() } AppContent()
val coroutineScope = rememberCoroutineScope() }
}
Surface( // Проверяем разрешения при запуске
modifier = Modifier.fillMaxSize(), checkPermissionsAndStart()
color = MaterialTheme.colorScheme.background
) { // Запускаем фоновый сервис
Scaffold( startConnectionService()
snackbarHost = { SnackbarHost(snackbarHostState) } }
) { paddingValues ->
@Composable
private fun AppContent() {
var currentScreen by remember { mutableStateOf(Screen.Main) }
when (currentScreen) {
Screen.Main -> {
MainScreen( MainScreen(
viewModel = viewModel, connectionManager = connectionManager,
onRequestPermissions = { autoApprovalManager = autoApprovalManager,
requestMissingPermissions() preferenceManager = preferenceManager,
}, permissionHelper = permissionHelper,
onShowError = { message -> onOpenSettings = { currentScreen = Screen.Settings }
coroutineScope.launch { )
snackbarHostState.showSnackbar(message)
}
} }
Screen.Settings -> {
SettingsScreen(
preferenceManager = preferenceManager,
permissionHelper = permissionHelper,
onNavigateBack = { currentScreen = Screen.Main }
) )
} }
} }
} }
}
private enum class Screen {
Main, Settings
} }
/** private fun checkPermissionsAndStart() {
* Проверить и запросить недостающие разрешения if (permissionHelper.hasAllPermissions()) {
*/ // Все разрешения уже есть, можно работать
private fun checkAndRequestPermissions() { Log.d("MainActivity", "All permissions granted")
if (!permissionManager.hasAllRequiredPermissions()) { startApplication()
Logger.d("Some permissions are missing, requesting...")
requestMissingPermissions()
} else { } else {
Logger.d("All required permissions are granted") // Запрашиваем разрешения
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()) {
private fun requestMissingPermissions() { connectionManager.connect()
val missingPermissions = permissionManager.getMissingPermissions()
if (missingPermissions.isNotEmpty()) {
Logger.d("Requesting permissions: ${missingPermissions.joinToString(", ")}")
permissionsLauncher.launch(missingPermissions.toTypedArray())
} }
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() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Logger.d("MainActivity destroyed")
// Очищаем ресурсы Activity, но оставляем сервис работать
connectionManager.cleanup()
autoApprovalManager.cleanup()
// Останавливаем сервис только если приложение действительно закрывается
if (isFinishing) {
stopConnectionService()
}
Log.d("MainActivity", "MainActivity destroyed, resources cleaned up")
} }
} }

View File

@@ -0,0 +1,470 @@
package com.example.godeye
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.core.content.edit
import com.example.godeye.managers.*
import com.example.godeye.models.*
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 kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
* MainViewModel - Главная ViewModel, интегрирующая все компоненты согласно ТЗ
* Архитектура: MVVM с LiveData/StateFlow
* Сеть: Socket.IO для сигнализации, WebRTC для медиа
*/
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val context = getApplication<Application>()
// Управляющие компоненты согласно ТЗ
private var socketService: SocketService? = null
private var sessionManager: SessionManager = SessionManager()
private var permissionManager: PermissionManager = PermissionManager(context)
private var camera2Manager: Camera2Manager = Camera2Manager(context)
private var webRTCManager: WebRTCManager? = null
// Состояния приложения
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
private val _serverUrl = MutableStateFlow("")
val serverUrl: StateFlow<String> = _serverUrl.asStateFlow()
private val _deviceId = MutableStateFlow("")
val deviceId: StateFlow<String> = _deviceId.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
// Управление сессиями согласно ТЗ
private val _cameraRequest = MutableStateFlow<CameraRequest?>(null)
val cameraRequest: StateFlow<CameraRequest?> = _cameraRequest.asStateFlow()
private val _activeSessions = MutableStateFlow<Map<String, SessionInfo>>(emptyMap())
val activeSessions: StateFlow<Map<String, SessionInfo>> = _activeSessions.asStateFlow()
private val _isStreaming = MutableStateFlow(false)
val isStreaming: StateFlow<Boolean> = _isStreaming.asStateFlow()
// Разрешения согласно ТЗ
val permissionsGranted = permissionManager.permissionsGranted
val missingPermissions = permissionManager.missingPermissions
// Камеры согласно ТЗ
val availableCameras = camera2Manager.availableCameras
val cameraState = camera2Manager.cameraState
// Подключение к SocketService
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Logger.step("SERVICE_CONNECTED", "SocketService connected")
val binder = service as SocketService.LocalBinder
socketService = binder.getService()
setupServiceObservers()
}
override fun onServiceDisconnected(name: ComponentName?) {
Logger.step("SERVICE_DISCONNECTED", "SocketService disconnected")
socketService = null
}
}
init {
Logger.step("VIEWMODEL_INIT", "MainViewModel initialization with full ТЗ architecture")
initializeApp()
bindToSocketService()
}
/**
* Инициализация приложения согласно ТЗ
*/
private fun initializeApp() {
Logger.step("APP_INIT", "Initializing application with ТЗ requirements")
// 1. Проверка разрешений (CAMERA, RECORD_AUDIO, INTERNET, FOREGROUND_SERVICE)
permissionManager.checkPermissions()
// 2. Генерация/загрузки Device ID
val prefs = context.getPreferences()
var deviceId = prefs.getString("device_id", null)
if (deviceId == null) {
deviceId = generateDeviceId()
prefs.edit { putString("device_id", deviceId) }
}
_deviceId.value = deviceId
// 3. Загрузка сохраненного URL сервера
val savedUrl = prefs.getString("server_url", "http://192.168.219.108:3001") ?: ""
_serverUrl.value = savedUrl
// 4. Инициализация WebRTC
initializeWebRTC()
Logger.step("APP_INIT_COMPLETE", "Application initialized according to ТЗ")
Logger.d("Configuration:")
Logger.d(" Device ID: $deviceId")
Logger.d(" Server URL: $savedUrl")
Logger.d(" Available cameras: ${camera2Manager.getAvailableCameraTypes()}")
Logger.d(" Permissions granted: ${permissionManager.permissionsGranted.value}")
}
/**
* Подключение к SocketService для фоновой работы
*/
private fun bindToSocketService() {
val intent = Intent(context, SocketService::class.java)
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
/**
* Настройка наблюдателей за SocketService - исправленная версия
*/
private fun setupServiceObservers() {
val service = socketService ?: return
// Простое наблюдение за состоянием подключения без внутренних API
viewModelScope.launch {
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 {
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 {
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
}
}
}
}
/**
* Инициализация WebRTC согласно ТЗ
*/
private fun initializeWebRTC() {
webRTCManager = WebRTCManager(context)
}
/**
* Подключение к серверу согласно ТЗ (Socket.IO)
*/
fun connectToServer() {
Logger.step("CONNECT_TO_SERVER", "Connecting to backend server via Socket.IO")
if (!permissionManager.checkCriticalPermissions()) {
Logger.error("CONNECT_FAILED", "Critical permissions not granted", null)
return
}
val url = _serverUrl.value
if (url.isBlank()) {
Logger.error("CONNECT_FAILED", "Server URL is empty", null)
return
}
_isLoading.value = true
viewModelScope.launch {
try {
// Запуск SocketService для фоновой работы
val intent = Intent(context, SocketService::class.java)
context.startForegroundService(intent)
// Подключение через SocketService
socketService?.connect(url, _deviceId.value)
// Сохранение URL
context.getPreferences().edit {
putString("server_url", url)
}
Logger.step("CONNECT_INITIATED", "Connection initiated to: $url")
} catch (e: Exception) {
Logger.error("CONNECT_ERROR", "Failed to connect to server", e)
_connectionState.value = ConnectionState.ERROR
} finally {
_isLoading.value = false
}
}
}
/**
* Принятие запроса камеры согласно ТЗ
*/
fun acceptCameraRequest(sessionId: String, reason: String = "Accepted by user") {
Logger.step("ACCEPT_CAMERA_REQUEST", "Accepting camera request: $sessionId")
val request = _cameraRequest.value
if (request?.sessionId != sessionId) {
Logger.error("ACCEPT_FAILED", "Invalid session ID", null)
return
}
viewModelScope.launch {
try {
// 1. Создание сессии в SessionManager
sessionManager.createSession(sessionId, request.operatorId, request.cameraType)
// 2. Отправка положительного ответа через SocketService
socketService?.sendCameraResponse(sessionId, true, reason)
// 3. Инициализация WebRTC соединения
webRTCManager?.startStreaming(request.operatorId, request.cameraType)
// 4. Очистка запроса
_cameraRequest.value = null
Logger.step("CAMERA_REQUEST_ACCEPTED", "Camera request accepted for session: $sessionId")
} catch (e: Exception) {
Logger.error("ACCEPT_REQUEST_ERROR", "Failed to accept camera request", e)
}
}
}
/**
* Отклонение запроса камеры
*/
fun rejectCameraRequest(sessionId: String, reason: String = "Rejected by user") {
Logger.step("REJECT_CAMERA_REQUEST", "Rejecting camera request: $sessionId")
socketService?.sendCameraResponse(sessionId, false, reason)
_cameraRequest.value = null
Logger.step("CAMERA_REQUEST_REJECTED", "Camera request rejected: $sessionId")
}
/**
* Обработка WebRTC событий
*/
private fun handleWebRTCEvent(event: com.example.godeye.services.WebRTCEvent) {
when (event) {
is com.example.godeye.services.WebRTCEvent.Connected -> {
Logger.step("WEBRTC_CONNECTED", "WebRTC connected")
}
is com.example.godeye.services.WebRTCEvent.Disconnected -> {
Logger.step("WEBRTC_DISCONNECTED", "WebRTC disconnected")
}
is com.example.godeye.services.WebRTCEvent.Error -> {
Logger.step("WEBRTC_ERROR", "WebRTC error: ${event.message}")
}
else -> {
Logger.step("WEBRTC_EVENT", "Unknown WebRTC event: $event")
}
}
}
/**
* Остановка всех стримов
*/
fun stopAllStreaming() {
viewModelScope.launch {
try {
Logger.step("STOP_ALL_STREAMING", "Stopping all camera streaming")
webRTCManager?.stopAllStreaming()
_activeSessions.value = emptyMap()
_isStreaming.value = false
Logger.step("STOP_ALL_STREAMING_SUCCESS", "All streaming stopped successfully")
} catch (e: Exception) {
Logger.error("STOP_ALL_STREAMING_ERROR", "Failed to stop streaming", e)
}
}
}
/**
* Переключение камеры
*/
fun switchCamera(cameraType: String) {
viewModelScope.launch {
try {
Logger.step("SWITCH_CAMERA", "Switching camera to: $cameraType")
webRTCManager?.switchCamera()
// Обновляем тип камеры в активных сессиях
val updatedSessions = _activeSessions.value.mapValues { (_, sessionInfo) ->
sessionInfo.copy(cameraType = cameraType)
}
_activeSessions.value = updatedSessions
Logger.step("SWITCH_CAMERA_SUCCESS", "Camera switched to: $cameraType")
} catch (e: Exception) {
Logger.error("SWITCH_CAMERA_ERROR", "Failed to switch camera", e)
}
}
}
/**
* Завершение сессии
*/
fun endCameraSession(sessionId: String) {
Logger.step("END_SESSION", "Ending camera session: $sessionId")
sessionManager.endSession(sessionId, "Ended by user")
webRTCManager?.endSession()
Logger.step("SESSION_ENDED", "Session ended: $sessionId")
}
/**
* Отключение от сервера
*/
fun disconnect() {
Logger.step("DISCONNECT", "Disconnecting from server")
socketService?.disconnect()
sessionManager.endAllSessions("User disconnected")
webRTCManager?.stopAllStreaming()
_connectionState.value = ConnectionState.DISCONNECTED
_isStreaming.value = false
Logger.step("DISCONNECTED", "Disconnected from server")
}
/**
* Обновление URL сервера
*/
fun updateServerUrl(url: String) {
_serverUrl.value = url
context.getPreferences().edit {
putString("server_url", url)
}
Logger.step("SERVER_URL_UPDATED", "Server URL updated: $url")
}
/**
* Очистка запроса камеры
*/
fun clearCameraRequest() {
_cameraRequest.value = null
}
/**
* Проверка разрешений
*/
fun checkPermissions() {
permissionManager.checkPermissions()
}
/**
* Callback при получении разрешений
*/
fun onPermissionsGranted() {
Logger.step("VIEWMODEL_PERMISSIONS_GRANTED", "All permissions granted in ViewModel")
permissionManager.checkPermissions()
}
/**
* Запуск тестового стриминга камеры
*/
fun startTestStreaming() {
viewModelScope.launch {
try {
Logger.step("START_TEST_STREAMING", "Starting test camera streaming")
// Создаем тестовую сессию
val testSessionId = "test_session_${System.currentTimeMillis()}"
val currentTime = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
.format(java.util.Date())
val testSessionInfo = SessionInfo(
sessionId = testSessionId,
deviceId = _deviceId.value,
operatorId = "test_operator",
cameraType = "back",
status = "streaming",
createdAt = currentTime
)
// Добавляем в активные сессии
val currentSessions = _activeSessions.value.toMutableMap()
currentSessions[testSessionId] = testSessionInfo
_activeSessions.value = currentSessions
_isStreaming.value = true
// Инициализируем WebRTC если нужно
if (webRTCManager == null) {
webRTCManager = WebRTCManager(context)
}
// Запускаем стриминг
webRTCManager?.startStreaming("test_operator", "back")
Logger.step("START_TEST_STREAMING_SUCCESS", "Test streaming started successfully")
} catch (e: Exception) {
Logger.error("START_TEST_STREAMING_ERROR", "Failed to start test streaming", e)
}
}
}
override fun onCleared() {
Logger.step("VIEWMODEL_CLEARED", "MainViewModel cleared with ТЗ cleanup")
sessionManager.endAllSessions("App closed")
webRTCManager?.dispose()
camera2Manager.release()
super.onCleared()
}
}

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

@@ -0,0 +1,304 @@
package com.example.godeye.managers
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.SurfaceTexture
import android.hardware.camera2.*
import android.util.Size
import android.view.Surface
import com.example.godeye.models.AppError
import com.example.godeye.models.CameraInfo
import com.example.godeye.models.CameraState
import com.example.godeye.utils.Logger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
/**
* Camera2Manager - управление камерами устройства с использованием Camera2 API
* Соответствует требованиям ТЗ для работы с различными типами камер
*/
class Camera2Manager(private val context: Context) {
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
private var cameraDevice: CameraDevice? = null
private var captureSession: CameraCaptureSession? = null
private var currentCameraId: String? = null
private val cameraOpenCloseLock = Semaphore(1)
private val _cameraState = MutableStateFlow(CameraState.CLOSED)
val cameraState: StateFlow<CameraState> = _cameraState.asStateFlow()
private val _availableCameras = MutableStateFlow<List<CameraInfo>>(emptyList())
val availableCameras: StateFlow<List<CameraInfo>> = _availableCameras.asStateFlow()
init {
detectAvailableCameras()
}
/**
* Определение доступных камер устройства согласно ТЗ
* Поддерживает: back, front, wide, telephoto
*/
private fun detectAvailableCameras() {
Logger.step("CAMERA_DETECTION", "Detecting available cameras")
val cameras = mutableListOf<CameraInfo>()
try {
for (cameraId in cameraManager.cameraIdList) {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
val focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
val cameraType = when (facing) {
CameraCharacteristics.LENS_FACING_BACK -> {
// Определяем тип задней камеры по фокусному расстоянию
when {
focalLengths != null && focalLengths.size > 1 -> {
if (focalLengths.minOrNull()!! < 3.0f) "ultra_wide"
else if (focalLengths.maxOrNull()!! > 6.0f) "telephoto"
else "back"
}
else -> "back"
}
}
CameraCharacteristics.LENS_FACING_FRONT -> "front"
else -> "unknown"
}
if (cameraType != "unknown") {
val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val sizes = configMap?.getOutputSizes(SurfaceTexture::class.java) ?: emptyArray()
cameras.add(
CameraInfo(
id = cameraId,
type = cameraType,
facing = facing ?: -1,
supportedSizes = sizes.toList(),
focalLengths = focalLengths?.toList() ?: emptyList()
)
)
Logger.d("Camera detected: $cameraId, type: $cameraType, sizes: ${sizes.size}")
}
}
_availableCameras.value = cameras
Logger.step("CAMERA_DETECTION_COMPLETE", "Found ${cameras.size} cameras: ${cameras.map { it.type }}")
} catch (e: CameraAccessException) {
Logger.error("CAMERA_DETECTION_ERROR", "Failed to detect cameras", e)
}
}
/**
* Получение списка доступных типов камер для регистрации на сервере
*/
fun getAvailableCameraTypes(): List<String> {
return _availableCameras.value.map { it.type }.distinct()
}
/**
* Запуск камеры указанного типа
*/
@SuppressLint("MissingPermission")
fun startCamera(cameraType: String, surface: Surface, onError: (AppError) -> Unit) {
Logger.step("CAMERA_START", "Starting camera: $cameraType")
val cameraInfo = _availableCameras.value.find { it.type == cameraType }
if (cameraInfo == null) {
Logger.error("CAMERA_NOT_FOUND", "Camera type not available: $cameraType", null)
onError(AppError.CameraNotAvailable)
return
}
try {
if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
Logger.error("CAMERA_LOCK_TIMEOUT", "Camera lock timeout", null)
onError(AppError.CameraNotAvailable)
return
}
_cameraState.value = CameraState.OPENING
val stateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
Logger.step("CAMERA_OPENED", "Camera opened: ${camera.id}")
cameraOpenCloseLock.release()
cameraDevice = camera
currentCameraId = camera.id
_cameraState.value = CameraState.OPENED
createCaptureSession(surface, onError)
}
override fun onDisconnected(camera: CameraDevice) {
Logger.step("CAMERA_DISCONNECTED", "Camera disconnected: ${camera.id}")
cameraOpenCloseLock.release()
camera.close()
cameraDevice = null
currentCameraId = null
_cameraState.value = CameraState.CLOSED
}
override fun onError(camera: CameraDevice, error: Int) {
Logger.error("CAMERA_ERROR", "Camera error: $error for ${camera.id}", null)
cameraOpenCloseLock.release()
camera.close()
cameraDevice = null
currentCameraId = null
_cameraState.value = CameraState.ERROR
onError(AppError.CameraNotAvailable)
}
}
cameraManager.openCamera(cameraInfo.id, stateCallback, null)
} catch (e: CameraAccessException) {
Logger.error("CAMERA_START_ERROR", "Failed to start camera", e)
onError(AppError.CameraNotAvailable)
} catch (e: SecurityException) {
Logger.error("CAMERA_PERMISSION_ERROR", "Camera permission denied", e)
onError(AppError.CameraPermissionDenied)
}
}
/**
* Создание сессии захвата для передачи видео
*/
private fun createCaptureSession(surface: Surface, onError: (AppError) -> Unit) {
try {
val camera = cameraDevice ?: run {
onError(AppError.CameraNotAvailable)
return
}
_cameraState.value = CameraState.CONFIGURING
val sessionCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
Logger.step("CAPTURE_SESSION_CONFIGURED", "Capture session configured")
captureSession = session
_cameraState.value = CameraState.ACTIVE
startPreview(session, surface, onError)
}
override fun onConfigureFailed(session: CameraCaptureSession) {
Logger.error("CAPTURE_SESSION_FAILED", "Failed to configure capture session", null)
_cameraState.value = CameraState.ERROR
onError(AppError.CameraNotAvailable)
}
}
// Используем стандартный метод createCaptureSession вместо устаревшего
camera.createCaptureSession(listOf(surface), sessionCallback, null)
} catch (e: CameraAccessException) {
Logger.error("CAPTURE_SESSION_ERROR", "Failed to create capture session", e)
onError(AppError.CameraNotAvailable)
}
}
/**
* Запуск предварительного просмотра
*/
private fun startPreview(session: CameraCaptureSession, surface: Surface, onError: (AppError) -> Unit) {
try {
val camera = cameraDevice ?: run {
onError(AppError.CameraNotAvailable)
return
}
val previewRequestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
previewRequestBuilder.addTarget(surface)
// Настройки для оптимального качества видео
previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
val previewRequest = previewRequestBuilder.build()
session.setRepeatingRequest(previewRequest, object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureStarted(
session: CameraCaptureSession,
request: CaptureRequest,
timestamp: Long,
frameNumber: Long
) {
// Preview started
}
}, null)
Logger.step("CAMERA_PREVIEW_STARTED", "Camera preview started")
} catch (e: CameraAccessException) {
Logger.error("CAMERA_PREVIEW_ERROR", "Failed to start preview", e)
onError(AppError.CameraNotAvailable)
}
}
/**
* Переключение на другую камеру
*/
fun switchCamera(newCameraType: String, surface: Surface, onError: (AppError) -> Unit) {
Logger.step("CAMERA_SWITCH", "Switching camera to: $newCameraType")
stopCamera()
startCamera(newCameraType, surface, onError)
}
/**
* Остановка камеры
*/
fun stopCamera() {
Logger.step("CAMERA_STOP", "Stopping camera")
try {
cameraOpenCloseLock.acquire()
captureSession?.close()
captureSession = null
cameraDevice?.close()
cameraDevice = null
currentCameraId = null
_cameraState.value = CameraState.CLOSED
Logger.step("CAMERA_STOPPED", "Camera stopped")
} catch (e: InterruptedException) {
Logger.error("CAMERA_STOP_ERROR", "Interrupted while stopping camera", e)
} finally {
cameraOpenCloseLock.release()
}
}
/**
* Получение оптимального размера для WebRTC
*/
fun getOptimalSize(cameraType: String, maxWidth: Int = 1920, maxHeight: Int = 1080): Size? {
val cameraInfo = _availableCameras.value.find { it.type == cameraType } ?: return null
return cameraInfo.supportedSizes
.filter { it.width <= maxWidth && it.height <= maxHeight }
.maxByOrNull { it.width * it.height }
}
/**
* Получение текущего состояния камеры
*/
fun getCurrentCameraType(): String? {
val cameraId = currentCameraId ?: return null
return _availableCameras.value.find { it.id == cameraId }?.type
}
fun release() {
Logger.step("CAMERA_MANAGER_RELEASE", "Releasing Camera2Manager")
stopCamera()
}
}

View File

@@ -1,245 +0,0 @@
package com.example.godeye.managers
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.SurfaceTexture
import android.hardware.camera2.*
import android.media.MediaRecorder
import android.os.Handler
import android.os.HandlerThread
import android.util.Size
import android.view.Surface
import com.example.godeye.models.AppError
import com.example.godeye.utils.Constants
import com.example.godeye.utils.Logger
import com.example.godeye.utils.getCameraIdForType
import com.example.godeye.utils.getAvailableCameraTypes
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Менеджер для управления камерами устройства
*/
class CameraManager(private val context: Context) {
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as android.hardware.camera2.CameraManager
private var currentCameraId: String? = null
private var captureSession: CameraCaptureSession? = null
private var cameraDevice: CameraDevice? = null
private var backgroundThread: HandlerThread? = null
private var backgroundHandler: Handler? = null
private val _isRecording = MutableStateFlow(false)
val isRecording: StateFlow<Boolean> = _isRecording.asStateFlow()
private val _currentCameraType = MutableStateFlow<String?>(null)
val currentCameraType: StateFlow<String?> = _currentCameraType.asStateFlow()
private val _error = MutableStateFlow<AppError?>(null)
val error: StateFlow<AppError?> = _error.asStateFlow()
/**
* Инициализация фонового потока для камеры
*/
private fun startBackgroundThread() {
backgroundThread = HandlerThread("CameraBackground").also { it.start() }
backgroundHandler = Handler(backgroundThread?.looper!!)
}
/**
* Остановка фонового потока
*/
private fun stopBackgroundThread() {
backgroundThread?.quitSafely()
try {
backgroundThread?.join()
backgroundThread = null
backgroundHandler = null
} catch (e: InterruptedException) {
Logger.e("Error stopping background thread", e)
}
}
/**
* Получить список доступных типов камер
*/
fun getAvailableCameraTypes(): List<String> {
return cameraManager.getAvailableCameraTypes()
}
/**
* Открыть камеру указанного типа
*/
@SuppressLint("MissingPermission")
fun openCamera(cameraType: String, surface: Surface, onSuccess: () -> Unit = {}, onError: (AppError) -> Unit = {}) {
try {
val cameraId = cameraManager.getCameraIdForType(cameraType)
if (cameraId == null) {
val error = AppError.CameraError("Camera type $cameraType not available")
_error.value = error
onError(error)
return
}
startBackgroundThread()
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
Logger.d("Camera opened: $cameraId")
cameraDevice = camera
currentCameraId = cameraId
_currentCameraType.value = cameraType
createCameraPreviewSession(surface, onSuccess, onError)
}
override fun onDisconnected(camera: CameraDevice) {
Logger.d("Camera disconnected: $cameraId")
camera.close()
cameraDevice = null
currentCameraId = null
_currentCameraType.value = null
}
override fun onError(camera: CameraDevice, error: Int) {
Logger.e("Camera error: $error")
camera.close()
cameraDevice = null
currentCameraId = null
_currentCameraType.value = null
val appError = AppError.CameraError("Camera error: $error")
_error.value = appError
onError(appError)
}
}, backgroundHandler)
} catch (e: Exception) {
Logger.e("Error opening camera", e)
val error = AppError.CameraError("Failed to open camera: ${e.message}")
_error.value = error
onError(error)
}
}
/**
* Создать сессию предварительного просмотра камеры
*/
private fun createCameraPreviewSession(surface: Surface, onSuccess: () -> Unit, onError: (AppError) -> Unit) {
try {
val cameraDevice = this.cameraDevice ?: run {
val error = AppError.CameraError("Camera device is null")
_error.value = error
onError(error)
return
}
val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureRequestBuilder.addTarget(surface)
// Используем совместимый подход для всех версий Android
@Suppress("DEPRECATION")
cameraDevice.createCaptureSession(
listOf(surface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
captureSession = session
try {
captureRequestBuilder.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
)
val captureRequest = captureRequestBuilder.build()
session.setRepeatingRequest(captureRequest, null, backgroundHandler)
_isRecording.value = true
Logger.d("Camera preview session created successfully")
onSuccess()
} catch (e: Exception) {
Logger.e("Error starting camera preview", e)
val error = AppError.CameraError("Failed to start preview: ${e.message}")
_error.value = error
onError(error)
}
}
override fun onConfigureFailed(session: CameraCaptureSession) {
Logger.e("Camera capture session configuration failed")
val error = AppError.CameraError("Failed to configure capture session")
_error.value = error
onError(error)
}
},
backgroundHandler
)
} catch (e: Exception) {
Logger.e("Error creating camera preview session", e)
val error = AppError.CameraError("Failed to create preview session: ${e.message}")
_error.value = error
onError(error)
}
}
/**
* Переключить на другой тип камеры
*/
fun switchCamera(newCameraType: String, surface: Surface, onSuccess: () -> Unit = {}, onError: (AppError) -> Unit = {}) {
Logger.d("Switching camera from ${_currentCameraType.value} to $newCameraType")
closeCamera()
openCamera(newCameraType, surface, onSuccess, onError)
}
/**
* Получить оптимальный размер для предварительного просмотра
*/
fun getOptimalPreviewSize(cameraType: String): Size? {
return try {
val cameraId = cameraManager.getCameraIdForType(cameraType) ?: return null
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val outputSizes = map?.getOutputSizes(SurfaceTexture::class.java)
// Выбираем размер близкий к 1080p, но не превышающий его
outputSizes?.find { it.width <= 1920 && it.height <= 1080 }
?: outputSizes?.minByOrNull { it.width * it.height }
} catch (e: Exception) {
Logger.e("Error getting optimal preview size", e)
null
}
}
/**
* Закрыть текущую камеру
*/
fun closeCamera() {
try {
captureSession?.close()
captureSession = null
cameraDevice?.close()
cameraDevice = null
currentCameraId = null
_currentCameraType.value = null
_isRecording.value = false
stopBackgroundThread()
Logger.d("Camera closed successfully")
} catch (e: Exception) {
Logger.e("Error closing camera", e)
}
}
/**
* Проверить, открыта ли камера
*/
fun isCameraOpen(): Boolean {
return cameraDevice != null
}
/**
* Очистить ошибку
*/
fun clearError() {
_error.value = null
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,99 +4,218 @@ import android.Manifest
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.example.godeye.models.AppError
import com.example.godeye.utils.Logger import com.example.godeye.utils.Logger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/** /**
* Менеджер для управления разрешениями приложения * PermissionManager - управление разрешениями приложения
* Соответствует требованиям ТЗ для работы с CAMERA, RECORD_AUDIO, INTERNET
*/ */
class PermissionManager(private val context: Context) { class PermissionManager(private val context: Context) {
private val _permissionsGranted = MutableStateFlow(false)
val permissionsGranted: StateFlow<Boolean> = _permissionsGranted.asStateFlow()
private val _missingPermissions = MutableStateFlow<List<String>>(emptyList())
val missingPermissions: StateFlow<List<String>> = _missingPermissions.asStateFlow()
companion object { companion object {
/**
* Все необходимые разрешения согласно ТЗ
*/
val REQUIRED_PERMISSIONS = arrayOf( val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA, Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO, Manifest.permission.RECORD_AUDIO,
Manifest.permission.INTERNET, Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.WAKE_LOCK, Manifest.permission.WAKE_LOCK,
Manifest.permission.FOREGROUND_SERVICE, Manifest.permission.FOREGROUND_SERVICE
Manifest.permission.POST_NOTIFICATIONS ).apply {
) // Добавляем FOREGROUND_SERVICE_CAMERA для API 34+ (но в Android 10 недоступно)
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
val CAMERA_PERMISSIONS = arrayOf( // plus(Manifest.permission.FOREGROUND_SERVICE_CAMERA)
Manifest.permission.CAMERA, // }
Manifest.permission.RECORD_AUDIO
)
} }
/** /**
* Проверить, есть ли все необходимые разрешения * Критически важные разрешения для основной функциональности
*/ */
fun hasAllRequiredPermissions(): Boolean { val CRITICAL_PERMISSIONS = arrayOf(
return REQUIRED_PERMISSIONS.all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
/**
* Проверить разрешения для камеры
*/
fun hasCameraPermissions(): Boolean {
return CAMERA_PERMISSIONS.all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
/**
* Проверить конкретное разрешение
*/
fun hasPermission(permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
/**
* Получить список отсутствующих разрешений
*/
fun getMissingPermissions(): List<String> {
return REQUIRED_PERMISSIONS.filter { permission ->
ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED
}
}
/**
* Получить список отсутствующих разрешений для камеры
*/
fun getMissingCameraPermissions(): List<String> {
return CAMERA_PERMISSIONS.filter { permission ->
ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED
}
}
/**
* Проверить критические разрешения для основной функциональности
*/
fun hasCriticalPermissions(): Boolean {
val criticalPermissions = arrayOf(
Manifest.permission.CAMERA, Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO, Manifest.permission.RECORD_AUDIO,
Manifest.permission.INTERNET Manifest.permission.INTERNET
) )
}
return criticalPermissions.all { permission -> /**
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED * Проверка всех необходимых разрешений
*/
fun checkPermissions(): Boolean {
Logger.step("PERMISSION_CHECK", "Checking all required permissions")
val missing = mutableListOf<String>()
REQUIRED_PERMISSIONS.forEach { permission ->
if (!isPermissionGranted(permission)) {
missing.add(permission)
Logger.d("Missing permission: $permission")
}
}
_missingPermissions.value = missing
val allGranted = missing.isEmpty()
_permissionsGranted.value = allGranted
Logger.step("PERMISSION_CHECK_RESULT",
if (allGranted) "All permissions granted"
else "Missing ${missing.size} permissions: ${missing.joinToString()}")
return allGranted
}
/**
* Проверка критически важных разрешений
*/
fun checkCriticalPermissions(): Boolean {
val missing = CRITICAL_PERMISSIONS.filter { !isPermissionGranted(it) }
if (missing.isNotEmpty()) {
Logger.step("CRITICAL_PERMISSIONS_MISSING",
"Missing critical permissions: ${missing.joinToString()}")
return false
}
Logger.step("CRITICAL_PERMISSIONS_OK", "All critical permissions granted")
return true
}
/**
* Проверка отдельного разрешения
*/
fun isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
/**
* Проверка разрешения камеры
*/
fun hasCameraPermission(): Boolean {
return isPermissionGranted(Manifest.permission.CAMERA)
}
/**
* Проверка разрешения микрофона
*/
fun hasAudioPermission(): Boolean {
return isPermissionGranted(Manifest.permission.RECORD_AUDIO)
}
/**
* Проверка разрешений для WebRTC
*/
fun hasWebRTCPermissions(): Boolean {
return hasCameraPermission() && hasAudioPermission()
}
/**
* Получение списка отсутствующих разрешений для запроса
*/
fun getMissingPermissions(): Array<String> {
return REQUIRED_PERMISSIONS.filter { !isPermissionGranted(it) }.toTypedArray()
}
/**
* Получение списка критически важных отсутствующих разрешений
*/
fun getMissingCriticalPermissions(): Array<String> {
return CRITICAL_PERMISSIONS.filter { !isPermissionGranted(it) }.toTypedArray()
}
/**
* Обработка результата запроса разрешений
*/
fun onPermissionsResult(
permissions: Array<String>,
grantResults: IntArray
): PermissionResult {
Logger.step("PERMISSION_RESULT", "Processing permission request result")
val granted = mutableListOf<String>()
val denied = mutableListOf<String>()
permissions.forEachIndexed { index, permission ->
if (grantResults[index] == PackageManager.PERMISSION_GRANTED) {
granted.add(permission)
Logger.d("Permission granted: $permission")
} else {
denied.add(permission)
Logger.d("Permission denied: $permission")
}
}
// Обновляем состояние
checkPermissions()
val result = when {
denied.isEmpty() -> PermissionResult.AllGranted
denied.any { it in CRITICAL_PERMISSIONS } -> PermissionResult.CriticalDenied(denied)
else -> PermissionResult.SomeGranted(granted, denied)
}
Logger.step("PERMISSION_RESULT_PROCESSED",
"Result: ${result::class.simpleName}, granted: ${granted.size}, denied: ${denied.size}")
return result
}
/**
* Получение ошибки для отсутствующих разрешений
*/
fun getPermissionError(): AppError? {
return when {
!hasCameraPermission() -> AppError.CameraPermissionDenied
!hasAudioPermission() -> AppError.CameraPermissionDenied // Аудио тоже критично
else -> null
} }
} }
/** /**
* Логирование состояния разрешений * Получение человекочитаемого описания разрешения
*/ */
fun logPermissionsStatus() { fun getPermissionDescription(permission: String): String {
Logger.d("=== Permission Status ===") return when (permission) {
REQUIRED_PERMISSIONS.forEach { permission -> Manifest.permission.CAMERA -> "Доступ к камере для видеосвязи"
val granted = hasPermission(permission) Manifest.permission.RECORD_AUDIO -> "Доступ к микрофону для аудиосвязи"
Logger.d("$permission: ${if (granted) "GRANTED" else "DENIED"}") Manifest.permission.INTERNET -> "Доступ к интернету для подключения к серверу"
Manifest.permission.ACCESS_NETWORK_STATE -> "Проверка состояния сети"
Manifest.permission.WAKE_LOCK -> "Предотвращение засыпания устройства"
Manifest.permission.FOREGROUND_SERVICE -> "Работа в фоновом режиме"
// Manifest.permission.FOREGROUND_SERVICE_CAMERA -> "Фоновая работа с камерой" // Недоступно в Android 10
else -> "Системное разрешение"
}
}
/**
* Проверка необходимости объяснения разрешения
*/
fun shouldShowRationale(permission: String): Boolean {
// Для системных разрешений обычно не показываем rationale
return when (permission) {
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO -> true
else -> false
} }
Logger.d("All required permissions: ${hasAllRequiredPermissions()}")
Logger.d("Camera permissions: ${hasCameraPermissions()}")
Logger.d("Critical permissions: ${hasCriticalPermissions()}")
} }
} }
/**
* Результат запроса разрешений
*/
sealed class PermissionResult {
object AllGranted : PermissionResult()
data class SomeGranted(val granted: List<String>, val denied: List<String>) : PermissionResult()
data class CriticalDenied(val denied: List<String>) : PermissionResult()
}

View File

@@ -1,152 +1,35 @@
package com.example.godeye.managers package com.example.godeye.managers
import com.example.godeye.models.CameraSession import com.example.godeye.models.CameraSession
import com.example.godeye.utils.Logger
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
/**
* Менеджер для управления активными сессиями с операторами
*/
class SessionManager { class SessionManager {
private val _sessions = MutableStateFlow<Map<String, CameraSession>>(emptyMap())
val sessions: StateFlow<Map<String, CameraSession>> = _sessions.asStateFlow()
private val _activeSessions = MutableStateFlow<List<CameraSession>>(emptyList()) fun createSession(sessionId: String, operatorId: String, cameraType: String) {
val activeSessions: StateFlow<List<CameraSession>> = _activeSessions.asStateFlow() val session = CameraSession(
/**
* Добавить новую сессию
*/
fun addSession(sessionId: String, operatorId: String, cameraType: String) {
val newSession = CameraSession(
sessionId = sessionId, sessionId = sessionId,
operatorId = operatorId, operatorId = operatorId,
cameraType = cameraType, cameraType = cameraType,
startTime = System.currentTimeMillis(),
isActive = true, isActive = true,
webRTCConnected = false startTime = System.currentTimeMillis()
) )
val currentSessions = _activeSessions.value.toMutableList() val currentSessions = _sessions.value.toMutableMap()
// Удаляем существующую сессию с тем же ID, если есть currentSessions[sessionId] = session
currentSessions.removeAll { it.sessionId == sessionId } _sessions.value = currentSessions
currentSessions.add(newSession)
_activeSessions.value = currentSessions
Logger.d("Session added: $sessionId, operator: $operatorId, camera: $cameraType")
} }
/** fun endSession(sessionId: String, reason: String = "Session ended") {
* Обновить статус WebRTC соединения для сессии val currentSessions = _sessions.value.toMutableMap()
*/ currentSessions.remove(sessionId)
fun updateWebRTCStatus(sessionId: String, connected: Boolean) { _sessions.value = currentSessions
val currentSessions = _activeSessions.value.toMutableList()
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
if (sessionIndex != -1) {
currentSessions[sessionIndex] = currentSessions[sessionIndex].copy(
webRTCConnected = connected
)
_activeSessions.value = currentSessions
Logger.d("WebRTC status updated for session $sessionId: $connected")
}
} }
/** fun endAllSessions(reason: String = "All sessions ended") {
* Переключить камеру для сессии _sessions.value = emptyMap()
*/
fun switchCameraForSession(sessionId: String, newCameraType: String) {
val currentSessions = _activeSessions.value.toMutableList()
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
if (sessionIndex != -1) {
currentSessions[sessionIndex] = currentSessions[sessionIndex].copy(
cameraType = newCameraType
)
_activeSessions.value = currentSessions
Logger.d("Camera switched for session $sessionId to $newCameraType")
}
}
/**
* Завершить сессию
*/
fun endSession(sessionId: String) {
val currentSessions = _activeSessions.value.toMutableList()
val removed = currentSessions.removeAll { it.sessionId == sessionId }
if (removed) {
_activeSessions.value = currentSessions
Logger.d("Session ended: $sessionId")
}
}
/**
* Получить сессию по ID
*/
fun getSession(sessionId: String): CameraSession? {
return _activeSessions.value.find { it.sessionId == sessionId }
}
/**
* Проверить, есть ли активные сессии
*/
fun hasActiveSessions(): Boolean {
return _activeSessions.value.isNotEmpty()
}
/**
* Получить количество активных сессий
*/
fun getActiveSessionCount(): Int {
return _activeSessions.value.size
}
/**
* Завершить все сессии
*/
fun endAllSessions() {
val sessionIds = _activeSessions.value.map { it.sessionId }
_activeSessions.value = emptyList()
Logger.d("All sessions ended: ${sessionIds.joinToString(", ")}")
}
/**
* Получить текущий тип камеры для активной сессии
*/
fun getCurrentCameraType(): String? {
return _activeSessions.value.firstOrNull()?.cameraType
}
/**
* Проверить, подключен ли WebRTC для сессии
*/
fun isWebRTCConnected(sessionId: String): Boolean {
return getSession(sessionId)?.webRTCConnected ?: false
}
/**
* Получить статистику сессий
*/
fun getSessionStats(): SessionStats {
val sessions = _activeSessions.value
return SessionStats(
totalSessions = sessions.size,
connectedSessions = sessions.count { it.webRTCConnected },
activeSessions = sessions.count { it.isActive },
oldestSessionTime = sessions.minOfOrNull { it.startTime },
newestSessionTime = sessions.maxOfOrNull { it.startTime }
)
} }
} }
/**
* Статистика сессий
*/
data class SessionStats(
val totalSessions: Int,
val connectedSessions: Int,
val activeSessions: Int,
val oldestSessionTime: Long?,
val newestSessionTime: 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,26 @@
package com.example.godeye.models
import android.util.Size
/**
* Состояния камеры согласно ТЗ
*/
enum class CameraState {
CLOSED, // Камера закрыта
OPENING, // Камера открывается
OPENED, // Камера открыта
CONFIGURING, // Настройка сессии захвата
ACTIVE, // Камера активна и передает видео
ERROR // Ошибка камеры
}
/**
* Информация о камере устройства согласно ТЗ
*/
data class CameraInfo(
val id: String, // ID камеры в системе
val type: String, // Тип камеры: back, front, ultra_wide, telephoto
val facing: Int, // Направление камеры (LENS_FACING_*)
val supportedSizes: List<Size>, // Поддерживаемые разрешения
val focalLengths: List<Float> // Фокусные расстояния для определения типа
)

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

@@ -1,154 +1,57 @@
package com.example.godeye.models package com.example.godeye.models
import android.os.Build
/**
* Информация об устройстве для регистрации на сервере
*/
data class DeviceInfo(
val model: String = Build.MODEL,
val androidVersion: String = Build.VERSION.RELEASE,
val appVersion: String = "1.0.0", // Заменяем BuildConfig на хардкод для упрощения
val availableCameras: List<String>
)
/**
* Активная сессия с оператором
*/
data class CameraSession(
val sessionId: String,
val operatorId: String,
val cameraType: String,
val startTime: Long,
var isActive: Boolean = true,
var webRTCConnected: Boolean = false
)
/**
* Запрос доступа к камере от оператора
*/
data class CameraRequest(
val sessionId: String,
val operatorId: String,
val cameraType: String,
val timestamp: Long = System.currentTimeMillis()
)
/**
* Ответ на запрос доступа к камере
*/
data class CameraResponse(
val sessionId: String,
val accepted: Boolean,
val reason: String? = null
)
/**
* WebRTC Offer/Answer данные
*/
data class WebRTCMessage(
val sessionId: String,
val type: String, // "offer", "answer", "ice-candidate"
val sdp: String? = null,
val candidate: String? = null,
val sdpMid: String? = null,
val sdpMLineIndex: Int? = null
)
/**
* События Socket.IO
*/
sealed class SocketEvent {
data class RegisterAndroid(
val deviceId: String,
val deviceInfo: DeviceInfo
) : SocketEvent()
data class CameraRequest(
val sessionId: String,
val operatorId: String,
val cameraType: String
) : SocketEvent()
data class CameraResponse(
val sessionId: String,
val accepted: Boolean,
val reason: String? = null
) : SocketEvent()
data class CameraDisconnect(
val sessionId: String
) : SocketEvent()
data class CameraSwitch(
val sessionId: String,
val newCameraType: String
) : SocketEvent()
data class WebRTCOffer(
val sessionId: String,
val offer: String
) : SocketEvent()
data class WebRTCAnswer(
val sessionId: String,
val answer: String
) : SocketEvent()
data class WebRTCIceCandidate(
val sessionId: String,
val candidate: String,
val sdpMid: String,
val sdpMLineIndex: Int
) : SocketEvent()
}
/**
* Состояния подключения
*/
enum class ConnectionState { enum class ConnectionState {
DISCONNECTED, DISCONNECTED,
CONNECTING, CONNECTING,
CONNECTED, CONNECTED,
ERROR, RECONNECTING,
RECONNECTING ERROR
} }
/** data class CameraResponse(
* Состояния WebRTC соединения val sessionId: String,
*/ val accepted: Boolean,
enum class WebRTCConnectionState { val reason: String? = null,
NEW, val streamUrl: String? = null
CONNECTING,
CONNECTED,
DISCONNECTED,
FAILED,
CLOSED
}
/**
* Типы ошибок приложения
*/
sealed class AppError {
object NetworkError : AppError()
object CameraPermissionDenied : AppError()
object AudioPermissionDenied : AppError()
object CameraNotAvailable : AppError()
object WebRTCConnectionFailed : AppError()
data class SocketError(val message: String) : AppError()
data class CameraError(val message: String) : AppError()
data class UnknownError(val throwable: Throwable) : AppError()
}
/**
* UI состояние главного экрана
*/
data class MainScreenState(
val deviceId: String = "",
val serverUrl: String = "",
val connectionState: ConnectionState = ConnectionState.DISCONNECTED,
val activeSessions: List<CameraSession> = emptyList(),
val isLoading: Boolean = false,
val error: AppError? = null,
val showCameraRequest: CameraRequest? = null
) )
data class SessionInfo(
val sessionId: String,
val deviceId: String,
val operatorId: String,
val cameraType: String,
val status: String,
val createdAt: String,
val acceptedAt: String? = null,
val endedAt: String? = null
)
data class CameraSwitchRequest(
val sessionId: String,
val cameraType: String
)
object SocketEvents {
const val REGISTER_ANDROID = "register:android"
const val REGISTER_SUCCESS = "register:success"
const val REGISTER_ERROR = "register:error"
const val CAMERA_REQUEST = "camera:request"
const val CAMERA_RESPONSE = "camera:response"
const val CAMERA_SWITCH = "camera:switch"
const val CAMERA_DISCONNECT = "camera:disconnect"
const val SESSION_CREATED = "session:created"
const val SESSION_ACCEPTED = "session:accepted"
const val SESSION_REJECTED = "session:rejected"
const val SESSION_ENDED = "session:ended"
const val SERVER_HELLO = "server:hello"
const val WEBRTC_OFFER = "webrtc:offer"
const val WEBRTC_ANSWER = "webrtc:answer"
const val WEBRTC_ICE_CANDIDATE = "webrtc:ice-candidate"
const val DEVICE_CONNECTED = "device:connected"
const val DEVICE_DISCONNECTED = "device:disconnected"
const val HEARTBEAT = "heartbeat"
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

@@ -0,0 +1,51 @@
package com.example.godeye.models
/**
* Активная сессия камеры с оператором
* Соответствует требованиям ТЗ для управления WebRTC сессиями
*/
data class CameraSession(
val sessionId: String,
val operatorId: String,
var cameraType: String,
val startTime: Long,
var isActive: Boolean = true,
var webRTCConnected: Boolean = false
)
/**
* Запрос доступа к камере от оператора
* Получается через Socket.IO событие "camera:request"
*/
data class CameraRequest(
val sessionId: String,
val operatorId: String,
val cameraType: String
)
/**
* События Socket.IO для типизированной обработки
* Соответствует архитектуре ТЗ с WebSocket сигнализацией
*/
sealed class SocketEvent {
data class RegisterAndroid(val deviceId: String, val deviceInfo: DeviceInfo) : SocketEvent()
data class CameraRequestEvent(val sessionId: String, val operatorId: String, val cameraType: String) : SocketEvent()
data class CameraResponse(val sessionId: String, val accepted: Boolean, val reason: String = "") : SocketEvent()
data class WebRTCOffer(val sessionId: String, val offer: String) : SocketEvent()
data class WebRTCAnswer(val sessionId: String, val answer: String) : SocketEvent()
data class IceCandidate(val sessionId: String, val candidate: String, val sdpMid: String, val sdpMLineIndex: Int) : SocketEvent()
data class CameraSwitch(val sessionId: String, val cameraType: String) : SocketEvent()
data class SessionEnd(val sessionId: String, val reason: String) : SocketEvent()
}
/**
* Ошибки приложения согласно ТЗ
*/
sealed class AppError {
object NetworkError : AppError()
object CameraPermissionDenied : AppError()
object CameraNotAvailable : AppError()
object WebRTCConnectionFailed : AppError()
data class SocketError(val message: String) : AppError()
data class UnknownError(val throwable: Throwable) : AppError()
}

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

@@ -1,425 +0,0 @@
package com.example.godeye.services
import android.app.*
import android.content.Intent
import android.graphics.SurfaceTexture
import android.os.Binder
import android.os.IBinder
import android.view.Surface
import androidx.core.app.NotificationCompat
import com.example.godeye.MainActivity
import com.example.godeye.R
import com.example.godeye.managers.CameraManager
import com.example.godeye.managers.SessionManager
import com.example.godeye.managers.WebRTCManager
import com.example.godeye.models.AppError
import com.example.godeye.models.WebRTCConnectionState
import com.example.godeye.utils.Constants
import com.example.godeye.utils.Logger
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* Сервис для управления камерой и WebRTC соединениями
*/
class CameraService : Service() {
private val binder = LocalBinder()
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private lateinit var cameraManager: CameraManager
private lateinit var webRTCManager: WebRTCManager
private lateinit var sessionManager: SessionManager
// Surface для WebRTC видео
private var webRTCSurface: Surface? = null
private var surfaceTexture: SurfaceTexture? = null
// StateFlows для отслеживания состояния
private val _isActive = MutableStateFlow(false)
val isActive: StateFlow<Boolean> = _isActive.asStateFlow()
private val _error = MutableStateFlow<AppError?>(null)
val error: StateFlow<AppError?> = _error.asStateFlow()
// Callbacks для передачи WebRTC событий
private var onWebRTCOfferCreated: ((String, String) -> Unit)? = null // sessionId, offer
private var onWebRTCIceCandidateCreated: ((String, String, String, Int) -> Unit)? = null // sessionId, candidate, sdpMid, sdpMLineIndex
inner class LocalBinder : Binder() {
fun getService(): CameraService = this@CameraService
}
override fun onCreate() {
super.onCreate()
Logger.d("CameraService created")
cameraManager = CameraManager(this)
webRTCManager = WebRTCManager(this)
sessionManager = SessionManager()
// Инициализация WebRTC
webRTCManager.initialize()
createNotificationChannel()
observeManagerStates()
}
override fun onBind(intent: Intent?): IBinder = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(Constants.FOREGROUND_SERVICE_ID + 1, createNotification())
return START_STICKY
}
/**
* Установить callbacks для WebRTC событий
*/
fun setWebRTCCallbacks(
onOfferCreated: (String, String) -> Unit,
onIceCandidateCreated: (String, String, String, Int) -> Unit
) {
this.onWebRTCOfferCreated = onOfferCreated
this.onWebRTCIceCandidateCreated = onIceCandidateCreated
}
/**
* Начать камера сессию
*/
fun startCameraSession(sessionId: String, operatorId: String, cameraType: String) {
serviceScope.launch {
try {
Logger.d("Starting camera session: $sessionId, camera: $cameraType")
// Добавляем сессию в менеджер
sessionManager.addSession(sessionId, operatorId, cameraType)
// Создаем Surface для WebRTC
setupWebRTCSurface()
// Создаем WebRTC соединение
webRTCManager.createPeerConnection(
onOfferCreated = { offer ->
onWebRTCOfferCreated?.invoke(sessionId, offer)
},
onAnswerCreated = { answer ->
// Ответ не используется, так как мы создаем offer
},
onIceCandidateCreated = { candidate, sdpMid, sdpMLineIndex ->
onWebRTCIceCandidateCreated?.invoke(sessionId, candidate, sdpMid, sdpMLineIndex)
}
)
// Создаем локальные медиа треки
webRTCManager.createLocalMediaTracks(cameraType)
// Открываем камеру
webRTCSurface?.let { surface ->
cameraManager.openCamera(
cameraType = cameraType,
surface = surface,
onSuccess = {
Logger.d("Camera opened successfully for session: $sessionId")
_isActive.value = true
// Создаем WebRTC offer
webRTCManager.createOffer()
},
onError = { error ->
Logger.e("Failed to open camera for session: $sessionId")
_error.value = error
sessionManager.endSession(sessionId)
}
)
} ?: run {
val error = AppError.CameraError("WebRTC surface not available")
_error.value = error
sessionManager.endSession(sessionId)
}
} catch (e: Exception) {
Logger.e("Error starting camera session", e)
_error.value = AppError.UnknownError(e)
sessionManager.endSession(sessionId)
}
}
}
/**
* Обработать WebRTC answer
*/
fun handleWebRTCAnswer(sessionId: String, answer: String) {
serviceScope.launch {
try {
Logger.d("Handling WebRTC answer for session: $sessionId")
webRTCManager.handleAnswer(answer)
sessionManager.updateWebRTCStatus(sessionId, true)
} catch (e: Exception) {
Logger.e("Error handling WebRTC answer", e)
_error.value = AppError.WebRTCConnectionFailed
}
}
}
/**
* Добавить ICE candidate
*/
fun addIceCandidate(sessionId: String, candidate: String, sdpMid: String, sdpMLineIndex: Int) {
serviceScope.launch {
try {
Logger.d("Adding ICE candidate for session: $sessionId")
webRTCManager.addIceCandidate(candidate, sdpMid, sdpMLineIndex)
} catch (e: Exception) {
Logger.e("Error adding ICE candidate", e)
}
}
}
/**
* Переключить камеру
*/
fun switchCamera(sessionId: String, newCameraType: String) {
serviceScope.launch {
try {
Logger.d("Switching camera for session $sessionId to $newCameraType")
// Обновляем тип камеры в сессии
sessionManager.switchCameraForSession(sessionId, newCameraType)
// Переключаем камеру в WebRTC
webRTCManager.switchCamera(newCameraType)
// Переключаем физическую камеру
webRTCSurface?.let { surface ->
cameraManager.switchCamera(
newCameraType = newCameraType,
surface = surface,
onSuccess = {
Logger.d("Camera switched successfully to: $newCameraType")
},
onError = { error ->
Logger.e("Failed to switch camera to: $newCameraType")
_error.value = error
}
)
}
} catch (e: Exception) {
Logger.e("Error switching camera", e)
_error.value = AppError.CameraError("Failed to switch camera: ${e.message}")
}
}
}
/**
* Завершить сессию
*/
fun endSession(sessionId: String) {
serviceScope.launch {
try {
Logger.d("Ending session: $sessionId")
// Закрываем камеру
cameraManager.closeCamera()
// Закрываем WebRTC соединение
webRTCManager.close()
// Удаляем сессию
sessionManager.endSession(sessionId)
// Очищаем Surface
cleanupWebRTCSurface()
_isActive.value = false
Logger.d("Session ended successfully: $sessionId")
// Если нет активных сессий, останавливаем сервис
if (!sessionManager.hasActiveSessions()) {
stopSelf()
}
} catch (e: Exception) {
Logger.e("Error ending session", e)
}
}
}
/**
* Завершить все сессии
*/
fun endAllSessions() {
serviceScope.launch {
try {
Logger.d("Ending all sessions")
// Закрываем камеру
cameraManager.closeCamera()
// Закрываем WebRTC соединение
webRTCManager.close()
// Удаляем все сессии
sessionManager.endAllSessions()
// Очищаем Surface
cleanupWebRTCSurface()
_isActive.value = false
Logger.d("All sessions ended successfully")
stopSelf()
} catch (e: Exception) {
Logger.e("Error ending all sessions", e)
}
}
}
/**
* Настроить Surface для WebRTC
*/
private fun setupWebRTCSurface() {
try {
// Создаем SurfaceTexture для WebRTC
surfaceTexture = SurfaceTexture(0).apply {
setDefaultBufferSize(1280, 720)
}
webRTCSurface = Surface(surfaceTexture)
Logger.d("WebRTC surface created successfully")
} catch (e: Exception) {
Logger.e("Error creating WebRTC surface", e)
_error.value = AppError.CameraError("Failed to create WebRTC surface: ${e.message}")
}
}
/**
* Очистить WebRTC Surface
*/
private fun cleanupWebRTCSurface() {
try {
webRTCSurface?.release()
webRTCSurface = null
surfaceTexture?.release()
surfaceTexture = null
Logger.d("WebRTC surface cleaned up")
} catch (e: Exception) {
Logger.e("Error cleaning up WebRTC surface", e)
}
}
/**
* Наблюдать за состояниями менеджеров
*/
private fun observeManagerStates() {
serviceScope.launch {
// Наблюдаем за ошибками камеры
cameraManager.error.collect { error ->
error?.let {
_error.value = it
Logger.e("Camera manager error: $it")
}
}
}
serviceScope.launch {
// Наблюдаем за ошибками WebRTC
webRTCManager.error.collect { error ->
error?.let {
_error.value = it
Logger.e("WebRTC manager error: $it")
}
}
}
serviceScope.launch {
// Наблюдаем за состоянием WebRTC соединения
webRTCManager.connectionState.collect { state ->
Logger.d("WebRTC connection state: $state")
when (state) {
WebRTCConnectionState.CONNECTED -> {
// Обновляем статус всех активных сессий
sessionManager.activeSessions.value.forEach { session ->
sessionManager.updateWebRTCStatus(session.sessionId, true)
}
}
WebRTCConnectionState.FAILED,
WebRTCConnectionState.DISCONNECTED -> {
// Обновляем статус всех активных сессий
sessionManager.activeSessions.value.forEach { session ->
sessionManager.updateWebRTCStatus(session.sessionId, false)
}
}
else -> { /* Игнорируем другие состояния */ }
}
}
}
}
/**
* Получить менеджер сессий
*/
fun getSessionManager(): SessionManager = sessionManager
/**
* Создать канал уведомлений
*/
private fun createNotificationChannel() {
val channel = NotificationChannel(
"${Constants.NOTIFICATION_CHANNEL_ID}_camera",
"GodEye Camera Service",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Уведомления о работе камеры GodEye"
setShowBadge(false)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
/**
* Создать уведомление для foreground service
*/
private fun createNotification(): Notification {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val activeSessionsCount = sessionManager.getActiveSessionCount()
val statusText = if (activeSessionsCount > 0) {
"Активных сессий: $activeSessionsCount"
} else {
"Камера готова к работе"
}
return NotificationCompat.Builder(this, "${Constants.NOTIFICATION_CHANNEL_ID}_camera")
.setContentTitle("GodEye Camera")
.setContentText(statusText)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.build()
}
/**
* Очистить ошибку
*/
fun clearError() {
_error.value = null
cameraManager.clearError()
webRTCManager.clearError()
}
override fun onDestroy() {
super.onDestroy()
serviceScope.launch {
endAllSessions()
}
Logger.d("CameraService destroyed")
}
}

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

@@ -1,19 +1,17 @@
package com.example.godeye.services package com.example.godeye.services
import android.app.* import android.app.Notification
import android.content.Context import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.Binder import android.os.Binder
import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.example.godeye.MainActivity
import com.example.godeye.R import com.example.godeye.R
import com.example.godeye.managers.PermissionManager
import com.example.godeye.models.* import com.example.godeye.models.*
import com.example.godeye.utils.Constants
import com.example.godeye.utils.Logger import com.example.godeye.utils.Logger
import com.example.godeye.utils.generateDeviceId
import com.example.godeye.utils.getAvailableCameraTypes
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonObject import com.google.gson.JsonObject
import io.socket.client.IO import io.socket.client.IO
@@ -25,11 +23,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.net.URI import java.net.URI
/** /**
* Сервис для управления WebSocket соединением с backend сервером * SocketService - основной сервис для WebSocket соединения с backend сервером
* Работает в фоне и обеспечивает постоянное соединение с сервером
*/ */
class SocketService : Service() { class SocketService : Service() {
@@ -38,393 +38,363 @@ class SocketService : Service() {
private val gson = Gson() private val gson = Gson()
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var permissionManager: PermissionManager // Состояния сервиса
// StateFlows для отслеживания состояния
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED) private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow() val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
private val _deviceId = MutableStateFlow("") private val _deviceId = MutableStateFlow("")
val deviceId: StateFlow<String> = _deviceId.asStateFlow() val deviceId: StateFlow<String> = _deviceId.asStateFlow()
private val _error = MutableStateFlow<AppError?>(null) // События для передачи в UI
val error: StateFlow<AppError?> = _error.asStateFlow() private val _cameraRequests = MutableStateFlow<CameraRequest?>(null)
val cameraRequests: StateFlow<CameraRequest?> = _cameraRequests.asStateFlow()
// События для UI private val _webRTCEvents = MutableStateFlow<WebRTCEvent?>(null)
private val _cameraRequest = MutableStateFlow<CameraRequest?>(null) val webRTCEvents: StateFlow<WebRTCEvent?> = _webRTCEvents.asStateFlow()
val cameraRequest: StateFlow<CameraRequest?> = _cameraRequest.asStateFlow()
private val _webrtcOffer = MutableStateFlow<WebRTCMessage?>(null)
val webrtcOffer: StateFlow<WebRTCMessage?> = _webrtcOffer.asStateFlow()
private val _webrtcAnswer = MutableStateFlow<WebRTCMessage?>(null)
val webrtcAnswer: StateFlow<WebRTCMessage?> = _webrtcAnswer.asStateFlow()
private val _webrtcIceCandidate = MutableStateFlow<WebRTCMessage?>(null)
val webrtcIceCandidate: StateFlow<WebRTCMessage?> = _webrtcIceCandidate.asStateFlow()
private val _cameraSwitchRequest = MutableStateFlow<Pair<String, String>?>(null) // sessionId, newCameraType
val cameraSwitchRequest: StateFlow<Pair<String, String>?> = _cameraSwitchRequest.asStateFlow()
private val _sessionDisconnect = MutableStateFlow<String?>(null) // sessionId
val sessionDisconnect: StateFlow<String?> = _sessionDisconnect.asStateFlow()
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
fun getService(): SocketService = this@SocketService fun getService(): SocketService = this@SocketService
} }
override fun onBind(intent: Intent): IBinder = binder
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Logger.d("SocketService created") Logger.step("SOCKET_SERVICE_CREATE", "SocketService created")
permissionManager = PermissionManager(this)
_deviceId.value = generateDeviceId()
createNotificationChannel() createNotificationChannel()
startForeground(NOTIFICATION_ID, createNotification())
} }
override fun onBind(intent: Intent?): IBinder = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(Constants.FOREGROUND_SERVICE_ID, createNotification()) Logger.step("SOCKET_SERVICE_START", "SocketService started")
return START_STICKY return START_STICKY // Перезапускать сервис при убийстве системой
} }
/** /**
* Подключиться к серверу * Подключение к backend серверу по Socket.IO
*/ */
fun connect(serverUrl: String) { fun connect(serverUrl: String, deviceId: String) {
serviceScope.launch { Logger.step("SOCKET_CONNECT", "Connecting to server: $serverUrl")
try { _deviceId.value = deviceId
_connectionState.value = ConnectionState.CONNECTING _connectionState.value = ConnectionState.CONNECTING
Logger.d("Connecting to server: $serverUrl")
// Дополнительная проверка URL
if (serverUrl.isBlank()) {
Logger.e("Server URL is empty")
_connectionState.value = ConnectionState.ERROR
_error.value = AppError.NetworkError
return@launch
}
Logger.d("Creating URI from: $serverUrl")
val uri = URI.create(serverUrl)
Logger.d("URI created successfully: $uri")
Logger.d("Creating Socket.IO client")
val options = IO.Options().apply {
timeout = 10000 // Увеличиваем таймаут до 10 секунд
reconnection = true
reconnectionDelay = 2000 // Увеличиваем задержку между попытками
reconnectionAttempts = 3 // Уменьшаем количество попыток
forceNew = true // Принудительно создаваем новое соединение
}
socket = IO.socket(uri, options).apply {
Logger.d("Socket.IO client created, setting up listeners")
setupEventListeners()
Logger.d("Listeners set up, initiating connection")
connect()
Logger.d("Connection initiated")
}
// Добавляем таймаут для проверки подключения
launch {
kotlinx.coroutines.delay(15000) // Ждем 15 секунд
if (_connectionState.value == ConnectionState.CONNECTING) {
Logger.w("Connection timeout after 15 seconds")
_connectionState.value = ConnectionState.ERROR
_error.value = AppError.SocketError("Connection timeout - server may be unreachable")
socket?.disconnect()
}
}
} catch (e: Exception) {
Logger.e("Error connecting to server: ${e.message}", e)
_connectionState.value = ConnectionState.ERROR
_error.value = AppError.SocketError("Connection failed: ${e.message}")
}
}
}
/**
* Отключиться от сервера
*/
fun disconnect() {
serviceScope.launch { serviceScope.launch {
try { try {
socket?.disconnect() val uri = URI.create(serverUrl)
socket?.close() val options = IO.Options().apply {
socket = null timeout = 10000
_connectionState.value = ConnectionState.DISCONNECTED reconnection = true
Logger.d("Disconnected from server") reconnectionAttempts = 5
reconnectionDelay = 1000
}
socket = IO.socket(uri, options)
setupEventListeners()
socket?.connect()
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("Error disconnecting from server", e) Logger.error("SOCKET_CONNECT_ERROR", "Failed to connect to server", e)
_connectionState.value = ConnectionState.ERROR
} }
} }
} }
/** /**
* Настроить обработчики событий Socket.IO * Настройка обработчиков событий Socket.IO
*/ */
private fun setupEventListeners() { private fun setupEventListeners() {
socket?.apply { socket?.apply {
Logger.d("Setting up Socket.IO event listeners") // Событие подключения
on(Socket.EVENT_CONNECT) { on(Socket.EVENT_CONNECT) {
Logger.d("✅ Socket connected successfully") Logger.step("SOCKET_CONNECTED", "Connected to server")
_connectionState.value = ConnectionState.CONNECTED _connectionState.value = ConnectionState.CONNECTED
registerDevice() registerDevice()
} }
on(Socket.EVENT_DISCONNECT) { args -> // Событие отключения
val reason = args.firstOrNull()?.toString() ?: "unknown" on(Socket.EVENT_DISCONNECT) {
Logger.d("❌ Socket disconnected: $reason") Logger.step("SOCKET_DISCONNECTED", "Disconnected from server")
_connectionState.value = ConnectionState.DISCONNECTED _connectionState.value = ConnectionState.DISCONNECTED
} }
on(Socket.EVENT_CONNECT_ERROR) { args -> // Событие подключения (исправлено)
val error = args.firstOrNull()?.toString() ?: "Unknown connection error" on(Socket.EVENT_CONNECT) {
Logger.e("🔥 Socket connection error: $error") Logger.step("SOCKET_RECONNECTED", "Reconnected to server")
_connectionState.value = ConnectionState.ERROR _connectionState.value = ConnectionState.CONNECTED
_error.value = AppError.SocketError(error) registerDevice()
} }
on(Constants.SocketEvents.REGISTER_SUCCESS) { args -> // Успешная регистрация устройства
Logger.d("Device registered successfully") on("register:success") { args ->
val data = args.firstOrNull()?.toString() Logger.step("REGISTER_SUCCESS", "Device registered successfully")
Logger.d("Registration response: $data") val response = args[0] as JSONObject
Logger.d("Registration response: $response")
} }
on(Constants.SocketEvents.REGISTER_ERROR) { args -> // Запрос доступа к камере от оператора
val error = args.firstOrNull()?.toString() ?: "Registration failed" on("camera:request") { args ->
Logger.e("Device registration error: $error")
_error.value = AppError.SocketError(error)
}
on(Constants.SocketEvents.CAMERA_REQUEST) { args ->
try { try {
val data = JSONObject(args[0].toString()) val requestData = args[0] as JSONObject
val request = CameraRequest( val cameraRequest = CameraRequest(
sessionId = data.getString("sessionId"), sessionId = requestData.getString("sessionId"),
operatorId = data.getString("operatorId"), operatorId = requestData.getString("operatorId"),
cameraType = data.getString("cameraType") cameraType = requestData.getString("cameraType")
) )
Logger.d("Camera request received: $request")
_cameraRequest.value = request Logger.step("CAMERA_REQUEST_RECEIVED",
"Camera request from operator ${cameraRequest.operatorId} for ${cameraRequest.cameraType}")
_cameraRequests.value = cameraRequest
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("Error parsing camera request", e) Logger.error("CAMERA_REQUEST_PARSE_ERROR", "Failed to parse camera request", e)
} }
} }
on(Constants.SocketEvents.CAMERA_DISCONNECT) { args -> // Завершение сессии
on("camera:disconnect") { args ->
try { try {
val data = JSONObject(args[0].toString()) val data = args[0] as JSONObject
val sessionId = data.getString("sessionId") val sessionId = data.getString("sessionId")
Logger.d("Camera disconnect received for session: $sessionId") Logger.step("CAMERA_DISCONNECT", "Session $sessionId ended by operator")
_sessionDisconnect.value = sessionId
// Очистить текущий запрос если он совпадает
if (_cameraRequests.value?.sessionId == sessionId) {
_cameraRequests.value = null
}
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("Error parsing camera disconnect", e) Logger.error("CAMERA_DISCONNECT_PARSE_ERROR", "Failed to parse disconnect event", e)
} }
} }
on(Constants.SocketEvents.CAMERA_SWITCH) { args -> // WebRTC offer от оператора
on("webrtc:offer") { args ->
try { try {
val data = JSONObject(args[0].toString()) val data = args[0] as JSONObject
val sessionId = data.getString("sessionId") val event = WebRTCEvent.Offer(
val newCameraType = data.getString("newCameraType")
Logger.d("Camera switch request: $sessionId -> $newCameraType")
_cameraSwitchRequest.value = Pair(sessionId, newCameraType)
} catch (e: Exception) {
Logger.e("Error parsing camera switch", e)
}
}
on(Constants.SocketEvents.WEBRTC_OFFER) { args ->
try {
val data = JSONObject(args[0].toString())
val message = WebRTCMessage(
sessionId = data.getString("sessionId"), sessionId = data.getString("sessionId"),
type = "offer", offer = data.getString("offer")
sdp = data.getString("offer")
) )
Logger.d("WebRTC offer received for session: ${message.sessionId}")
_webrtcOffer.value = message Logger.step("WEBRTC_OFFER_RECEIVED", "WebRTC offer received for session ${event.sessionId}")
_webRTCEvents.value = event
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("Error parsing WebRTC offer", e) Logger.error("WEBRTC_OFFER_PARSE_ERROR", "Failed to parse WebRTC offer", e)
} }
} }
on(Constants.SocketEvents.WEBRTC_ICE_CANDIDATE) { args -> // WebRTC answer от оператора
on("webrtc:answer") { args ->
try { try {
val data = JSONObject(args[0].toString()) val data = args[0] as JSONObject
val message = WebRTCMessage( val event = WebRTCEvent.Answer(
sessionId = data.getString("sessionId"),
answer = data.getString("answer")
)
Logger.step("WEBRTC_ANSWER_RECEIVED", "WebRTC answer received for session ${event.sessionId}")
_webRTCEvents.value = event
} catch (e: Exception) {
Logger.error("WEBRTC_ANSWER_PARSE_ERROR", "Failed to parse WebRTC answer", e)
}
}
// ICE кандидаты
on("webrtc:ice-candidate") { args ->
try {
val data = args[0] as JSONObject
val event = WebRTCEvent.IceCandidate(
sessionId = data.getString("sessionId"), sessionId = data.getString("sessionId"),
type = "ice-candidate",
candidate = data.getString("candidate"), candidate = data.getString("candidate"),
sdpMid = data.getString("sdpMid"), sdpMid = data.getString("sdpMid"),
sdpMLineIndex = data.getInt("sdpMLineIndex") sdpMLineIndex = data.getInt("sdpMLineIndex")
) )
Logger.d("WebRTC ICE candidate received for session: ${message.sessionId}")
_webrtcIceCandidate.value = message Logger.step("WEBRTC_ICE_RECEIVED", "ICE candidate received for session ${event.sessionId}")
_webRTCEvents.value = event
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("Error parsing WebRTC ICE candidate", e) Logger.error("WEBRTC_ICE_PARSE_ERROR", "Failed to parse ICE candidate", e)
} }
} }
// Переключение камеры
on("camera:switch") { args ->
try {
val data = args[0] as JSONObject
val sessionId = data.getString("sessionId")
val cameraType = data.getString("cameraType")
Logger.step("CAMERA_SWITCH_REQUEST", "Camera switch request: $cameraType for session $sessionId")
// Отправить событие переключения камеры
val event = WebRTCEvent.SwitchCamera(sessionId, cameraType)
_webRTCEvents.value = event
} catch (e: Exception) {
Logger.error("CAMERA_SWITCH_PARSE_ERROR", "Failed to parse camera switch request", e)
}
}
// Ошибки соединения
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)
_connectionState.value = ConnectionState.ERROR
}
} }
} }
/** /**
* Зарегистрировать устройство на сервере * Регистрация Android устройства на сервере
*/ */
private fun registerDevice() { private fun registerDevice() {
try {
val cameraManager = getSystemService(Context.CAMERA_SERVICE) as android.hardware.camera2.CameraManager
val deviceInfo = DeviceInfo( val deviceInfo = DeviceInfo(
availableCameras = cameraManager.getAvailableCameraTypes() deviceId = _deviceId.value,
deviceName = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}",
androidVersion = android.os.Build.VERSION.RELEASE,
appVersion = "1.0.0"
) )
val registrationData = JsonObject().apply { val registerData = JSONObject().apply {
addProperty("deviceId", _deviceId.value) put("deviceId", _deviceId.value)
add("deviceInfo", gson.toJsonTree(deviceInfo)) put("deviceInfo", JSONObject().apply {
put("model", deviceInfo.deviceName)
put("manufacturer", deviceInfo.manufacturer)
put("androidVersion", deviceInfo.androidVersion)
put("appVersion", deviceInfo.appVersion)
put("availableCameras", JSONArray().apply {
deviceInfo.availableCameras.forEach { put(it) }
})
})
} }
socket?.emit(Constants.SocketEvents.REGISTER_ANDROID, registrationData) socket?.emit("register:android", registerData)
Logger.d("Device registration sent: $registrationData") Logger.step("REGISTER_DEVICE", "Device registration sent: ${deviceInfo.deviceName}")
} catch (e: Exception) {
Logger.e("Error registering device", e)
_error.value = AppError.SocketError("Failed to register device: ${e.message}")
}
} }
/** /**
* Отправить ответ на запрос камеры * Отправка ответа на запрос камеры
*/ */
fun sendCameraResponse(sessionId: String, accepted: Boolean, reason: String? = null) { fun sendCameraResponse(sessionId: String, accepted: Boolean, reason: String = "") {
try { val responseData = JSONObject().apply {
val response = JsonObject().apply { put("sessionId", sessionId)
addProperty("sessionId", sessionId) put("accepted", accepted)
addProperty("accepted", accepted) if (!accepted && reason.isNotEmpty()) {
reason?.let { addProperty("reason", it) } put("reason", reason)
}
} }
socket?.emit(Constants.SocketEvents.CAMERA_RESPONSE, response) socket?.emit("camera:response", responseData)
Logger.d("Camera response sent: $response") Logger.step("CAMERA_RESPONSE_SENT", "Camera response sent: sessionId=$sessionId, accepted=$accepted")
} catch (e: Exception) {
Logger.e("Error sending camera response", e)
_error.value = AppError.SocketError("Failed to send camera response: ${e.message}")
}
} }
/** /**
* Отправить WebRTC answer * Отправка WebRTC offer
*/
fun sendWebRTCOffer(sessionId: String, offer: String) {
val offerData = JSONObject().apply {
put("sessionId", sessionId)
put("offer", offer)
}
socket?.emit("webrtc:offer", offerData)
Logger.step("WEBRTC_OFFER_SENT", "WebRTC offer sent for session $sessionId")
}
/**
* Отправка WebRTC answer
*/ */
fun sendWebRTCAnswer(sessionId: String, answer: String) { fun sendWebRTCAnswer(sessionId: String, answer: String) {
try { val answerData = JSONObject().apply {
val data = JsonObject().apply { put("sessionId", sessionId)
addProperty("sessionId", sessionId) put("answer", answer)
addProperty("answer", answer)
} }
socket?.emit(Constants.SocketEvents.WEBRTC_ANSWER, data) socket?.emit("webrtc:answer", answerData)
Logger.d("WebRTC answer sent for session: $sessionId") Logger.step("WEBRTC_ANSWER_SENT", "WebRTC answer sent for session $sessionId")
} catch (e: Exception) {
Logger.e("Error sending WebRTC answer", e)
_error.value = AppError.SocketError("Failed to send WebRTC answer: ${e.message}")
}
} }
/** /**
* Отправить ICE candidate * Отправка ICE кандидата
*/ */
fun sendIceCandidate(sessionId: String, candidate: String, sdpMid: String, sdpMLineIndex: Int) { fun sendIceCandidate(sessionId: String, candidate: String, sdpMid: String, sdpMLineIndex: Int) {
try { val candidateData = JSONObject().apply {
val data = JsonObject().apply { put("sessionId", sessionId)
addProperty("sessionId", sessionId) put("candidate", candidate)
addProperty("candidate", candidate) put("sdpMid", sdpMid)
addProperty("sdpMid", sdpMid) put("sdpMLineIndex", sdpMLineIndex)
addProperty("sdpMLineIndex", sdpMLineIndex)
} }
socket?.emit(Constants.SocketEvents.WEBRTC_ICE_CANDIDATE, data) socket?.emit("webrtc:ice-candidate", candidateData)
Logger.d("ICE candidate sent for session: $sessionId") Logger.step("WEBRTC_ICE_SENT", "ICE candidate sent for session $sessionId")
} catch (e: Exception) {
Logger.e("Error sending ICE candidate", e)
}
} }
/** /**
* Создать канал уведомлений * Отключение от сервера
*/ */
fun disconnect() {
Logger.step("SOCKET_DISCONNECT", "Disconnecting from server")
socket?.disconnect()
socket = null
_connectionState.value = ConnectionState.DISCONNECTED
}
private fun createNotificationChannel() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel( val channel = NotificationChannel(
Constants.NOTIFICATION_CHANNEL_ID, CHANNEL_ID,
"GodEye Service", "GodEye Service",
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
description = "Уведомления о состоянии подключения GodEye" description = "Сервис подключения к серверу"
setShowBadge(false) setShowBadge(false)
} }
val notificationManager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel) manager.createNotificationChannel(channel)
}
} }
/**
* Создать уведомление для foreground service
*/
private fun createNotification(): Notification { private fun createNotification(): Notification {
val intent = Intent(this, MainActivity::class.java) return NotificationCompat.Builder(this, CHANNEL_ID)
val pendingIntent = PendingIntent.getActivity( .setContentTitle("GodEye")
this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE .setContentText("Подключено к серверу")
)
val statusText = when (_connectionState.value) {
ConnectionState.CONNECTED -> "Подключено"
ConnectionState.CONNECTING -> "Подключение..."
ConnectionState.RECONNECTING -> "Переподключение..."
ConnectionState.DISCONNECTED -> "Отключено"
ConnectionState.ERROR -> "Ошибка подключения"
}
return NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_ID)
.setContentTitle("GodEye Signal Center")
.setContentText("Статус: $statusText")
.setSmallIcon(R.drawable.ic_launcher_foreground) .setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent)
.setOngoing(true) .setOngoing(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .setCategory(NotificationCompat.CATEGORY_SERVICE)
.build() .build()
} }
/**
* Обновить уведомление
*/
private fun updateNotification() {
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(Constants.FOREGROUND_SERVICE_ID, createNotification())
}
/**
* Очистить события
*/
fun clearCameraRequest() {
_cameraRequest.value = null
}
fun clearWebRTCOffer() {
_webrtcOffer.value = null
}
fun clearError() {
_error.value = null
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() Logger.step("SOCKET_SERVICE_DESTROY", "SocketService destroyed")
disconnect() disconnect()
Logger.d("SocketService destroyed") super.onDestroy()
}
companion object {
private const val CHANNEL_ID = "godeye_service_channel"
private const val NOTIFICATION_ID = 1001
} }
} }
/**
* События 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(
val sessionId: String,
val candidate: String,
val sdpMid: String,
val sdpMLineIndex: Int
) : WebRTCEvent()
data class SwitchCamera(val sessionId: String, val cameraType: String) : WebRTCEvent()
}

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

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

@@ -1,164 +0,0 @@
package com.example.godeye.ui.components
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.R
import com.example.godeye.models.CameraRequest
import com.example.godeye.utils.Constants
/**
* Диалог запроса доступа к камере от оператора
*/
@Composable
fun CameraRequestDialog(
request: CameraRequest,
onAccept: () -> Unit,
onDeny: () -> Unit,
onDismiss: () -> Unit
) {
var rememberChoice by remember { mutableStateOf(false) }
// Лог открытия диалога
LaunchedEffect(Unit) {
Log.d("GodEye", "CameraRequestDialog открыт: sessionId=${request.sessionId}, operatorId=${request.operatorId}, cameraType=${request.cameraType}")
}
Dialog(onDismissRequest = {
Log.d("GodEye", "Диалог закрыт пользователем")
onDismiss()
}) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Иконка камеры
Icon(
imageVector = Icons.Default.Person, // Заменено с PhotoCamera на Person
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
// Заголовок
Text(
text = stringResource(R.string.camera_request_title),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
// Основное сообщение
Text(
text = stringResource(
R.string.camera_request_message,
request.operatorId,
getCameraTypeName(request.cameraType)
),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
// ID сессии
Text(
text = stringResource(R.string.session_id_label, request.sessionId),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
// Чекбокс "Запомнить выбор"
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = rememberChoice,
onCheckedChange = { rememberChoice = it }
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.remember_choice),
style = MaterialTheme.typography.bodyMedium
)
}
Spacer(modifier = Modifier.height(24.dp))
// Кнопки
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Кнопка "Отклонить"
OutlinedButton(
onClick = {
Log.d("GodEye", "Пользователь отклонил запрос камеры")
onDeny()
},
modifier = Modifier
.weight(1f)
.height(48.dp)
) {
Text(stringResource(R.string.deny_button))
}
// Кнопка "Разрешить"
Button(
onClick = {
Log.d("GodEye", "Пользователь разрешил доступ к камере")
onAccept()
},
modifier = Modifier
.weight(1f)
.height(48.dp)
) {
Text(stringResource(R.string.allow_button))
}
}
}
}
}
}
/**
* Получить локализованное название типа камеры
*/
@Composable
private fun getCameraTypeName(cameraType: String): String {
return when (cameraType) {
Constants.CameraTypes.BACK -> stringResource(R.string.camera_type_back)
Constants.CameraTypes.FRONT -> stringResource(R.string.camera_type_front)
Constants.CameraTypes.WIDE -> stringResource(R.string.camera_type_wide)
Constants.CameraTypes.TELEPHOTO -> stringResource(R.string.camera_type_telephoto)
else -> cameraType
}
}

View File

@@ -1,146 +0,0 @@
package com.example.godeye.ui.components
import androidx.compose.animation.core.*
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.draw.rotate
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.godeye.R
import com.example.godeye.models.ConnectionState
/**
* Компонент для отображения статуса подключения к серверу
*/
@Composable
fun ConnectionStatusCard(
connectionState: ConnectionState,
modifier: Modifier = Modifier
) {
val (icon, color, statusText) = getConnectionStateInfo(connectionState)
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(
containerColor = color.copy(alpha = 0.1f)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Иконка с анимацией для состояний загрузки
ConnectionIcon(
icon = icon,
color = color,
isAnimated = connectionState == ConnectionState.CONNECTING ||
connectionState == ConnectionState.RECONNECTING
)
Spacer(modifier = Modifier.width(12.dp))
// Текст статуса
Column {
Text(
text = stringResource(R.string.connection_status_label),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = statusText,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = color
)
}
}
}
}
/**
* Анимированная иконка подключения
*/
@Composable
private fun ConnectionIcon(
icon: ImageVector,
color: androidx.compose.ui.graphics.Color,
isAnimated: Boolean,
modifier: Modifier = Modifier
) {
if (isAnimated) {
val infiniteTransition = rememberInfiniteTransition(label = "connection_animation")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "rotation"
)
Icon(
imageVector = icon,
contentDescription = null,
modifier = modifier
.size(24.dp)
.rotate(rotation),
tint = color
)
} else {
Icon(
imageVector = icon,
contentDescription = null,
modifier = modifier.size(24.dp),
tint = color
)
}
}
/**
* Получить информацию о состоянии подключения
*/
@Composable
private fun getConnectionStateInfo(
connectionState: ConnectionState
): Triple<ImageVector, androidx.compose.ui.graphics.Color, String> {
return when (connectionState) {
ConnectionState.DISCONNECTED -> Triple(
Icons.Default.Close,
MaterialTheme.colorScheme.onSurfaceVariant,
stringResource(R.string.status_disconnected)
)
ConnectionState.CONNECTING -> Triple(
Icons.Default.Refresh,
MaterialTheme.colorScheme.primary,
stringResource(R.string.status_connecting)
)
ConnectionState.CONNECTED -> Triple(
Icons.Default.CheckCircle,
MaterialTheme.colorScheme.primary,
stringResource(R.string.status_connected)
)
ConnectionState.ERROR -> Triple(
Icons.Default.Warning,
MaterialTheme.colorScheme.error,
stringResource(R.string.status_error)
)
ConnectionState.RECONNECTING -> Triple(
Icons.Default.Refresh,
MaterialTheme.colorScheme.secondary,
stringResource(R.string.status_reconnecting)
)
}
}

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

@@ -1,237 +0,0 @@
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.Person
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.godeye.R
import com.example.godeye.models.CameraSession
import com.example.godeye.utils.Constants
import kotlinx.coroutines.delay
import java.text.SimpleDateFormat
import java.util.*
/**
* Компонент для отображения списка активных сессий
*/
@Composable
fun SessionsList(
sessions: List<CameraSession>,
onEndSession: (String) -> Unit,
modifier: Modifier = Modifier
) {
if (sessions.isEmpty()) {
// Пустое состояние
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.no_active_sessions),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
LazyColumn(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(sessions, key = { it.sessionId }) { session ->
SessionItem(
session = session,
onEndSession = { onEndSession(session.sessionId) }
)
}
}
}
}
/**
* Элемент списка сессий
*/
@Composable
fun SessionItem(
session: CameraSession,
onEndSession: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// Заголовок с оператором
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "${stringResource(R.string.session_operator_label)} ${session.operatorId}",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
// Статус WebRTC
WebRTCStatusChip(isConnected = session.webRTCConnected)
}
Spacer(modifier = Modifier.height(12.dp))
// Информация о камере
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Person, // Заменено с Camera на Person
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "${stringResource(R.string.session_camera_label)} ${getCameraTypeName(session.cameraType)}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(8.dp))
// Длительность сессии
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
SessionDuration(startTime = session.startTime)
// Кнопка завершения сессии
FilledTonalButton(
onClick = onEndSession,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer
)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(stringResource(R.string.end_session_button))
}
}
}
}
}
/**
* Компонент для отображения статуса WebRTC соединения
*/
@Composable
fun WebRTCStatusChip(
isConnected: Boolean,
modifier: Modifier = Modifier
) {
val backgroundColor = if (isConnected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.errorContainer
}
val contentColor = if (isConnected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onErrorContainer
}
val statusText = if (isConnected) {
stringResource(R.string.webrtc_connected)
} else {
stringResource(R.string.webrtc_disconnected)
}
Surface(
modifier = modifier,
shape = RoundedCornerShape(16.dp),
color = backgroundColor
) {
Text(
text = stringResource(R.string.session_webrtc_status, statusText),
style = MaterialTheme.typography.labelSmall,
color = contentColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
/**
* Компонент для отображения длительности сессии
*/
@Composable
fun SessionDuration(
startTime: Long,
modifier: Modifier = Modifier
) {
var currentTime by remember { mutableStateOf(System.currentTimeMillis()) }
// Обновляем время каждую секунду
LaunchedEffect(startTime) {
while (true) {
currentTime = System.currentTimeMillis()
delay(1000)
}
}
val duration = currentTime - startTime
val hours = (duration / (1000 * 60 * 60)) % 24
val minutes = (duration / (1000 * 60)) % 60
val seconds = (duration / 1000) % 60
Text(
text = "${stringResource(R.string.session_duration_label)} ${String.format("%02d:%02d:%02d", hours, minutes, seconds)}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = modifier
)
}
/**
* Получить локализованное название типа камеры
*/
@Composable
private fun getCameraTypeName(cameraType: String): String {
return when (cameraType) {
Constants.CameraTypes.BACK -> stringResource(R.string.camera_type_back)
Constants.CameraTypes.FRONT -> stringResource(R.string.camera_type_front)
Constants.CameraTypes.WIDE -> stringResource(R.string.camera_type_wide)
Constants.CameraTypes.TELEPHOTO -> stringResource(R.string.camera_type_telephoto)
else -> cameraType
}
}

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"
}
}

File diff suppressed because it is too large Load Diff

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

@@ -1,11 +0,0 @@
package com.example.godeye.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,74 @@
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 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 Connected = Color(0xFF4CAF50)
val Disconnected = Color(0xFFF44336)
val Waiting = Color(0xFFFF9800)
}
private val DarkColorScheme = darkColorScheme(
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.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
fun GodEyeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) {
DarkColorScheme
} else {
LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(),
content = content
)
}

View File

@@ -1,58 +0,0 @@
package com.example.godeye.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun GodEyeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -1,34 +0,0 @@
package com.example.godeye.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@@ -1,385 +0,0 @@
package com.example.godeye.ui.viewmodels
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.example.godeye.managers.PermissionManager
import com.example.godeye.models.*
import com.example.godeye.services.CameraService
import com.example.godeye.services.SocketService
import com.example.godeye.utils.Constants
import com.example.godeye.utils.Logger
import com.example.godeye.utils.getPreferences
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
/**
* ViewModel для главного экрана приложения
*/
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val context = getApplication<Application>()
private val permissionManager = PermissionManager(context)
// Сервисы
private var socketService: SocketService? = null
private var cameraService: CameraService? = null
private var socketServiceBound = false
private var cameraServiceBound = false
// UI State
private val _uiState = MutableStateFlow(MainScreenState())
val uiState: StateFlow<MainScreenState> = _uiState.asStateFlow()
// События для UI
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
init {
loadSavedSettings()
// startServices() убран отсюда
}
/**
* Загрузить сохраненные настройки
*/
private fun loadSavedSettings() {
val prefs = context.getPreferences()
val serverUrl = prefs.getString(Constants.PreferenceKeys.SERVER_URL, Constants.DEFAULT_SERVER_URL) ?: Constants.DEFAULT_SERVER_URL
val deviceId = prefs.getString(Constants.PreferenceKeys.DEVICE_ID, "") ?: ""
_uiState.value = _uiState.value.copy(
serverUrl = serverUrl,
deviceId = deviceId
)
}
/**
* Запустить сервисы (вызывать из MainActivity после проверки разрешений)
*/
fun startServices() {
// Запуск SocketService
val socketIntent = Intent(context, SocketService::class.java)
context.startForegroundService(socketIntent)
context.bindService(socketIntent, socketConnection, Context.BIND_AUTO_CREATE)
// Запуск CameraService
val cameraIntent = Intent(context, CameraService::class.java)
context.startForegroundService(cameraIntent)
context.bindService(cameraIntent, cameraConnection, Context.BIND_AUTO_CREATE)
}
/**
* ServiceConnection для SocketService
*/
private val socketConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as SocketService.LocalBinder
socketService = binder.getService()
socketServiceBound = true
Logger.d("SocketService connected")
observeSocketService()
}
override fun onServiceDisconnected(arg0: ComponentName) {
socketServiceBound = false
socketService = null
Logger.d("SocketService disconnected")
}
}
/**
* ServiceConnection для CameraService
*/
private val cameraConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as CameraService.LocalBinder
cameraService = binder.getService()
cameraServiceBound = true
Logger.d("CameraService connected")
observeCameraService()
setupCameraServiceCallbacks()
}
override fun onServiceDisconnected(arg0: ComponentName) {
cameraServiceBound = false
cameraService = null
Logger.d("CameraService disconnected")
}
}
/**
* Наблюдать за состоянием SocketService
*/
private fun observeSocketService() {
val service = socketService ?: return
viewModelScope.launch {
service.connectionState.collect { state ->
_uiState.value = _uiState.value.copy(connectionState = state)
}
}
viewModelScope.launch {
service.deviceId.collect { deviceId ->
_uiState.value = _uiState.value.copy(deviceId = deviceId)
}
}
viewModelScope.launch {
service.error.collect { error ->
error?.let {
_uiState.value = _uiState.value.copy(error = it)
_events.emit(UiEvent.ShowError(it))
}
}
}
viewModelScope.launch {
service.cameraRequest.collect { request ->
request?.let {
_uiState.value = _uiState.value.copy(showCameraRequest = it)
_events.emit(UiEvent.ShowCameraRequestDialog(it))
}
}
}
viewModelScope.launch {
service.webrtcOffer.collect { offer ->
offer?.let {
handleWebRTCOffer(it)
service.clearWebRTCOffer()
}
}
}
viewModelScope.launch {
service.webrtcIceCandidate.collect { candidate ->
candidate?.let {
handleWebRTCIceCandidate(it)
}
}
}
viewModelScope.launch {
service.cameraSwitchRequest.collect { request ->
request?.let { (sessionId, cameraType) ->
handleCameraSwitch(sessionId, cameraType)
}
}
}
viewModelScope.launch {
service.sessionDisconnect.collect { sessionId ->
sessionId?.let {
handleSessionDisconnect(it)
}
}
}
}
/**
* Наблюдать за состоянием CameraService
*/
private fun observeCameraService() {
val service = cameraService ?: return
viewModelScope.launch {
service.getSessionManager().activeSessions.collect { sessions ->
_uiState.value = _uiState.value.copy(activeSessions = sessions)
}
}
viewModelScope.launch {
service.error.collect { error ->
error?.let {
_uiState.value = _uiState.value.copy(error = it)
_events.emit(UiEvent.ShowError(it))
}
}
}
}
/**
* Настроить callbacks для CameraService
*/
private fun setupCameraServiceCallbacks() {
cameraService?.setWebRTCCallbacks(
onOfferCreated = { sessionId, offer ->
// WebRTC offer создан, но в нашем случае мы получаем offer от оператора
Logger.d("WebRTC offer created for session: $sessionId")
},
onIceCandidateCreated = { sessionId, candidate, sdpMid, sdpMLineIndex ->
socketService?.sendIceCandidate(sessionId, candidate, sdpMid, sdpMLineIndex)
}
)
}
/**
* Подключиться к серверу
*/
fun connect() {
if (!permissionManager.hasCriticalPermissions()) {
_events.tryEmit(UiEvent.RequestPermissions)
return
}
val serverUrl = _uiState.value.serverUrl
if (serverUrl.isBlank()) {
_events.tryEmit(UiEvent.ShowError(AppError.SocketError("Введите URL сервера")))
return
}
// Сохраняем URL сервера
context.getPreferences().edit()
.putString(Constants.PreferenceKeys.SERVER_URL, serverUrl)
.apply()
_uiState.value = _uiState.value.copy(isLoading = true)
socketService?.connect(serverUrl)
}
/**
* Отключиться от сервера
*/
fun disconnect() {
_uiState.value = _uiState.value.copy(isLoading = true)
socketService?.disconnect()
cameraService?.endAllSessions()
}
/**
* Обновить URL сервера
*/
fun updateServerUrl(url: String) {
_uiState.value = _uiState.value.copy(serverUrl = url)
}
/**
* Ответить на запрос камеры
*/
fun respondToCameraRequest(sessionId: String, accepted: Boolean, reason: String? = null) {
socketService?.sendCameraResponse(sessionId, accepted, reason)
if (accepted) {
// Получаем информацию о запросе
val request = _uiState.value.showCameraRequest
if (request != null && request.sessionId == sessionId) {
// Начинаем камера сессию
cameraService?.startCameraSession(sessionId, request.operatorId, request.cameraType)
}
}
// Очищаем запрос из UI
_uiState.value = _uiState.value.copy(showCameraRequest = null)
socketService?.clearCameraRequest()
}
/**
* Завершить сессию
*/
fun endSession(sessionId: String) {
cameraService?.endSession(sessionId)
}
/**
* Обработать WebRTC Offer
*/
private fun handleWebRTCOffer(offer: WebRTCMessage) {
val sessionId = offer.sessionId
val offerSdp = offer.sdp ?: return
Logger.d("Handling WebRTC offer for session: $sessionId")
// В нашем случае мы не обрабатываем offer, так как создаем его сами
// Но можно добавить логику для обработки offer от оператора
}
/**
* Обработать WebRTC ICE Candidate
*/
private fun handleWebRTCIceCandidate(candidate: WebRTCMessage) {
val sessionId = candidate.sessionId
val candidateSdp = candidate.candidate ?: return
val sdpMid = candidate.sdpMid ?: return
val sdpMLineIndex = candidate.sdpMLineIndex ?: return
Logger.d("Handling ICE candidate for session: $sessionId")
cameraService?.addIceCandidate(sessionId, candidateSdp, sdpMid, sdpMLineIndex)
}
/**
* Обработать переключение камеры
*/
private fun handleCameraSwitch(sessionId: String, newCameraType: String) {
Logger.d("Handling camera switch for session $sessionId to $newCameraType")
cameraService?.switchCamera(sessionId, newCameraType)
}
/**
* Обработать отключение сессии
*/
private fun handleSessionDisconnect(sessionId: String) {
Logger.d("Handling session disconnect: $sessionId")
cameraService?.endSession(sessionId)
}
/**
* Очистить ошибку
*/
fun clearError() {
_uiState.value = _uiState.value.copy(error = null)
socketService?.clearError()
cameraService?.clearError()
}
/**
* Проверить разрешения
*/
fun checkPermissions(): Boolean {
return permissionManager.hasAllRequiredPermissions()
}
/**
* Получить отсутствующие разрешения
*/
fun getMissingPermissions(): List<String> {
return permissionManager.getMissingPermissions()
}
override fun onCleared() {
super.onCleared()
// Отвязка сервисов
try {
if (socketServiceBound) {
context.unbindService(socketConnection)
socketServiceBound = false
}
if (cameraServiceBound) {
context.unbindService(cameraConnection)
cameraServiceBound = false
}
} catch (e: Exception) {
Logger.e("Error unbinding services", e)
}
Logger.d("MainViewModel cleared")
}
}
/**
* События UI для обработки в Activity/Compose
*/
sealed class UiEvent {
object RequestPermissions : UiEvent()
data class ShowError(val error: AppError) : UiEvent()
data class ShowCameraRequestDialog(val request: CameraRequest) : UiEvent()
data class ShowMessage(val message: String) : UiEvent()
}

View File

@@ -1,49 +1,133 @@
package com.example.godeye.utils package com.example.godeye.utils
object Constants { import android.content.Context
// WebSocket события
object SocketEvents {
const val REGISTER_ANDROID = "register:android"
const val REGISTER_SUCCESS = "register:success"
const val REGISTER_ERROR = "register:error"
const val CAMERA_REQUEST = "camera:request"
const val CAMERA_RESPONSE = "camera:response"
const val CAMERA_DISCONNECT = "camera:disconnect"
const val CAMERA_SWITCH = "camera:switch"
const val WEBRTC_OFFER = "webrtc:offer"
const val WEBRTC_ANSWER = "webrtc:answer"
const val WEBRTC_ICE_CANDIDATE = "webrtc:ice-candidate"
}
// Типы камер /**
* Constants - константы приложения согласно ТЗ
*/
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.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
const val SOCKET_RECONNECTION_ATTEMPTS = 5
const val SOCKET_RECONNECTION_DELAY = 1000L
// Настройки WebRTC согласно ТЗ
const val WEBRTC_VIDEO_WIDTH = 1920
const val WEBRTC_VIDEO_HEIGHT = 1080
const val WEBRTC_VIDEO_FPS = 30
// STUN серверы согласно ТЗ
val STUN_SERVERS = listOf(
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302"
)
// Типы камер согласно ТЗ
object CameraTypes { object CameraTypes {
const val BACK = "back" const val BACK = "back"
const val FRONT = "front" const val FRONT = "front"
const val WIDE = "wide" const val ULTRA_WIDE = "ultra_wide"
const val TELEPHOTO = "telephoto" const val TELEPHOTO = "telephoto"
val ALL_TYPES = listOf(BACK, FRONT, ULTRA_WIDE, TELEPHOTO)
} }
// SharedPreferences ключи // События Socket.IO согласно ТЗ
object SocketEvents {
// Исходящие события
const val REGISTER_ANDROID = "register:android"
const val CAMERA_RESPONSE = "camera:response"
const val WEBRTC_OFFER = "webrtc:offer"
const val WEBRTC_ANSWER = "webrtc:answer"
const val WEBRTC_ICE_CANDIDATE = "webrtc:ice-candidate"
// Входящие события
const val REGISTER_SUCCESS = "register:success"
const val CAMERA_REQUEST = "camera:request"
const val CAMERA_DISCONNECT = "camera:disconnect"
const val CAMERA_SWITCH = "camera:switch"
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 ключи согласно ТЗ
object PreferenceKeys { object PreferenceKeys {
const val SERVER_URL = "server_url" const val SERVER_URL = "server_url"
const val DEVICE_ID = "device_id" const val DEVICE_ID = "device_id"
const val AUTO_ACCEPT_REQUESTS = "auto_accept_requests" const val AUTO_ACCEPT_REQUESTS = "auto_accept_requests"
const val CAMERA_QUALITY = "camera_quality" const val CAMERA_QUALITY = "camera_quality"
const val NOTIFICATION_ENABLED = "notification_enabled" const val NOTIFICATION_ENABLED = "notification_enabled"
const val REMEMBER_OPERATORS = "remember_operators"
} }
// Настройки по умолчанию // Настройки уведомлений
const val DEFAULT_SERVER_URL = "http://10.0.2.2:3001" // Специальный IP для Android эмулятора
const val SOCKET_CONNECTION_TIMEOUT = 10000L
const val WEBRTC_CONNECTION_TIMEOUT = 15000L
// Уведомления
const val NOTIFICATION_CHANNEL_ID = "godeye_service_channel" const val NOTIFICATION_CHANNEL_ID = "godeye_service_channel"
const val FOREGROUND_SERVICE_ID = 1001 const val SERVICE_NOTIFICATION_ID = 1001
const val CAMERA_REQUEST_NOTIFICATION_ID = 1002
// WebRTC настройки // Таймауты
val STUN_SERVERS = listOf( const val CAMERA_OPEN_TIMEOUT = 2500L
"stun:stun.l.google.com:19302", const val WEBRTC_CONNECTION_TIMEOUT = 10000L
"stun:stun1.l.google.com:19302" const val SOCKET_CONNECTION_TIMEOUT = 5000L
)
// Версии API
const val MIN_SDK_VERSION = 24 // Android 7.0+ согласно ТЗ
const val TARGET_SDK_VERSION = 34
// Качество видео
object VideoQuality {
const val HD_WIDTH = 1280
const val HD_HEIGHT = 720
const val FULL_HD_WIDTH = 1920
const val FULL_HD_HEIGHT = 1080
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

@@ -0,0 +1,14 @@
package com.example.godeye.utils
class ErrorHandler {
fun handle(error: Throwable) {
// Простая обработка ошибок: логирование
println("Error: ${error.message}")
}
fun handleUncaughtException(thread: Thread, exception: Throwable) {
// Обработка неперехваченных исключений
println("Uncaught exception in thread ${thread.name}: ${exception.message}")
exception.printStackTrace()
}
}

View File

@@ -2,139 +2,157 @@ package com.example.godeye.utils
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.hardware.camera2.CameraCharacteristics import android.net.wifi.WifiManager
import android.hardware.camera2.CameraManager import android.net.ConnectivityManager
import android.util.Log import android.net.NetworkCapabilities
import androidx.compose.runtime.Composable import java.net.NetworkInterface
import androidx.compose.runtime.LaunchedEffect import java.net.InetAddress
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import java.util.* import java.util.*
import java.text.SimpleDateFormat
/**
* Расширения для Context
*/
fun Context.getPreferences(): SharedPreferences { fun Context.getPreferences(): SharedPreferences {
return getSharedPreferences("godeye_prefs", Context.MODE_PRIVATE) return getSharedPreferences("godeye_prefs", Context.MODE_PRIVATE)
} }
fun Context.generateDeviceId(): String { fun generateDeviceId(): String {
val prefs = getPreferences() return "android_${UUID.randomUUID().toString().take(8)}"
var deviceId = prefs.getString(Constants.PreferenceKeys.DEVICE_ID, null)
if (deviceId == null) {
deviceId = "android_${UUID.randomUUID().toString().take(8)}"
prefs.edit().putString(Constants.PreferenceKeys.DEVICE_ID, deviceId).apply()
}
return deviceId
} }
/** /**
* Расширения для CameraManager * НОВОЕ: Получение реального IP-адреса устройства с обновленными API
*/ */
fun CameraManager.getAvailableCameraTypes(): List<String> { fun Context.getLocalIpAddress(): String {
val cameras = mutableListOf<String>()
try { try {
for (cameraId in cameraIdList) { // Метод 1: Через WiFi Manager (для WiFi соединений) - обновленный API
val characteristics = getCameraCharacteristics(cameraId) val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
val facing = characteristics.get(CameraCharacteristics.LENS_FACING) wifiManager?.let { wifi ->
if (wifi.isWifiEnabled) {
when (facing) { // ИСПРАВЛЕНИЕ: Используем современный API для получения WiFi информации
CameraCharacteristics.LENS_FACING_BACK -> { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
// Проверяем на широкоугольный и телеобъектив // Для Android 10+ используем ConnectivityManager
val focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
if (focalLengths != null && focalLengths.isNotEmpty()) { connectivityManager?.let { cm ->
val minFocalLength = focalLengths.minOrNull() ?: 0f val activeNetwork = cm.activeNetwork
val maxFocalLength = focalLengths.maxOrNull() ?: 0f val linkProperties = cm.getLinkProperties(activeNetwork)
linkProperties?.linkAddresses?.forEach { linkAddress ->
when { val address = linkAddress.address
minFocalLength < 2.8f -> cameras.add(Constants.CameraTypes.WIDE) if (address is java.net.Inet4Address && !address.isLoopbackAddress) {
maxFocalLength > 5.5f -> cameras.add(Constants.CameraTypes.TELEPHOTO) val ip = address.hostAddress
else -> cameras.add(Constants.CameraTypes.BACK) if (ip != null && !ip.startsWith("127.")) {
Logger.step("IP_DETECTION", "Modern API IP detected: $ip")
return ip
}
}
}
} }
} else { } else {
cameras.add(Constants.CameraTypes.BACK) // Для старых версий 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
} }
} }
CameraCharacteristics.LENS_FACING_FRONT -> cameras.add(Constants.CameraTypes.FRONT)
} }
} }
// Метод 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) { } catch (e: Exception) {
Log.e("CameraExtensions", "Error getting cameras", e) Logger.error("IP_DETECTION_ERROR", "Failed to detect local IP", e)
// Добавляем базовые камеры как fallback
cameras.add(Constants.CameraTypes.BACK)
cameras.add(Constants.CameraTypes.FRONT)
} }
return cameras.distinct()
// Fallback: возвращаем IP по умолчанию для вашей подсети
val fallbackIP = "192.168.219.108" // ИСПРАВЛЕНИЕ: Изменено на ваш сервер
Logger.step("IP_DETECTION_FALLBACK", "Using fallback IP: $fallbackIP")
return fallbackIP
} }
fun CameraManager.getCameraIdForType(cameraType: String): String? { /**
* НОВОЕ: Получение IP сервера из URL
*/
fun extractServerIP(serverUrl: String): String {
return try { return try {
for (cameraId in cameraIdList) { val regex = """://([^:/]+)""".toRegex()
val characteristics = getCameraCharacteristics(cameraId) val match = regex.find(serverUrl)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING) match?.groupValues?.get(1) ?: "192.168.219.1" // Fallback на ваш роутер
when (cameraType) {
Constants.CameraTypes.FRONT -> {
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
return cameraId
}
}
Constants.CameraTypes.BACK,
Constants.CameraTypes.WIDE,
Constants.CameraTypes.TELEPHOTO -> {
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
// Для простоты используем первую найденную заднюю камеру
// В реальном проекте здесь была бы более сложная логика
return cameraId
}
}
}
}
null
} catch (e: Exception) { } catch (e: Exception) {
Log.e("CameraExtensions", "Error finding camera for type $cameraType", e) Logger.error("SERVER_IP_EXTRACTION_ERROR", "Failed to extract server IP from URL: $serverUrl", e)
null "192.168.219.1"
} }
} }
/** /**
* Compose расширения для Flow * НОВОЕ: Автоматическое определение подсети
*/ */
@Composable fun Context.detectNetworkSubnet(): String {
fun <T> Flow<T>.collectAsEffect( val localIP = getLocalIpAddress()
key: Any? = null, return try {
action: suspend (T) -> Unit val parts = localIP.split(".")
) { if (parts.size >= 3) {
val lifecycleOwner = LocalLifecycleOwner.current "${parts[0]}.${parts[1]}.${parts[2]}.0/24"
LaunchedEffect(key) { } else {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { "192.168.219.0/24" // Fallback на вашу подсеть
collect(action)
} }
} catch (e: Exception) {
Logger.error("SUBNET_DETECTION_ERROR", "Failed to detect subnet", e)
"192.168.219.0/24"
} }
} }
/** /**
* Логирование * НОВОЕ: Проверка, находится ли IP в локальной сети
*/ */
object Logger { fun isLocalNetworkIP(ip: String): Boolean {
private const val TAG = "GodEye" return try {
val parts = ip.split(".").map { it.toInt() }
fun d(message: String, tag: String = TAG) { when {
Log.d(tag, message) // 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) {
fun e(message: String, throwable: Throwable? = null, tag: String = TAG) { false
Log.e(tag, message, throwable)
}
fun i(message: String, tag: String = TAG) {
Log.i(tag, message)
}
fun w(message: String, tag: String = TAG) {
Log.w(tag, message)
} }
} }

View File

@@ -0,0 +1,75 @@
package com.example.godeye.utils
import android.util.Log
object Logger {
private const val TAG = "GodEye"
fun d(message: String) {
Log.d(TAG, "🔍 $message")
println("🔍 [DEBUG] $message")
}
fun i(message: String) {
Log.i(TAG, " $message")
println(" [INFO] $message")
}
fun w(message: String) {
Log.w(TAG, "⚠️ $message")
println("⚠️ [WARN] $message")
}
fun e(message: String, throwable: Throwable? = null) {
Log.e(TAG, "$message", throwable)
println("❌ [ERROR] $message")
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")
}
fun socket(message: String) {
Log.d(TAG, "🔌 SOCKET: $message")
println("🔌 SOCKET: $message")
}
fun connection(message: String) {
Log.d(TAG, "🌐 CONNECTION: $message")
println("🌐 CONNECTION: $message")
}
fun registration(message: String) {
Log.d(TAG, "📱 REGISTRATION: $message")
println("📱 REGISTRATION: $message")
}
fun camera(message: String) {
Log.d(TAG, "📷 CAMERA: $message")
println("📷 CAMERA: $message")
}
fun network(message: String) {
Log.d(TAG, "🌍 NETWORK: $message")
println("🌍 NETWORK: $message")
}
}

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

@@ -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,16 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#80FFFFFF" />
<stroke
android:width="2dp"
android:color="#FFFFFF" />
<size
android:width="80dp"
android:height="80dp" />
</shape>

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"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="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 <path
android:fillColor="#3DDC84" android:pathData="M54,14A40,40 0,1 1,54,94A40,40 0,1 1,54,14Z"
android:pathData="M0,0h108v108h-108z" /> android:fillColor="@android:color/transparent"
android:strokeColor="#1A40E0D0"
android:strokeWidth="1"/>
<path <path
android:fillColor="#00000000" android:pathData="M54,4A50,50 0,1 1,54,104A50,50 0,1 1,54,4Z"
android:pathData="M9,0L9,108" android:fillColor="@android:color/transparent"
android:strokeWidth="0.8" android:strokeColor="#1A00BFFF"
android:strokeColor="#33FFFFFF" /> android:strokeWidth="0.5"/>
<path
android:fillColor="#00000000" <!-- Точки на фоне (заменены на path) -->
android:pathData="M19,0L19,108" <path android:pathData="M20,19a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
android:strokeWidth="0.8" <path android:pathData="M88,19a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
android:strokeColor="#33FFFFFF" /> <path android:pathData="M20,87a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
<path <path android:pathData="M88,87a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
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" />
</vector> </vector>

View File

@@ -4,27 +4,80 @@
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="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"> <!-- Фоновый градиент -->
<path
android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor"> <aapt:attr name="android:fillColor">
<gradient <gradient
android:endX="85.84757" android:startY="0"
android:endY="92.4963" android:startX="0"
android:startX="42.9492" android:endY="108"
android:startY="49.59793" android:endX="108"
android:type="linear"> android:type="linear">
<item <item android:offset="0" android:color="#0A1828"/>
android:color="#44000000" <item android:offset="0.5" android:color="#1E3A5F"/>
android:offset="0.0" /> <item android:offset="1" android:color="#2D5A87"/>
<item
android:color="#00000000"
android:offset="1.0" />
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </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 <path
android:fillColor="#FFFFFF" android:fillColor="#FFFFFF"
android:fillType="nonZero" android:pathData="M52,44C51,44 50,45 50,46C50,47 51,48 52,48C53,48 54,47 54,46C54,45 53,44 52,44Z"/>
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: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> </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,100 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<!-- Предварительный просмотр камеры -->
<SurfaceView
android:id="@+id/surfaceViewCamera"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
<!-- Верхняя панель с информацией -->
<LinearLayout
android:id="@+id/topPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="vertical"
android:background="#80000000"
android:padding="16dp">
<TextView
android:id="@+id/tvCameraInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📹 Legacy Camera для Android 9"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:gravity="center"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚪ Инициализация..."
android:textColor="#FFFFFF"
android:textSize="14sp"
android:gravity="center" />
</LinearLayout>
<!-- Нижняя панель с кнопками управления -->
<LinearLayout
android:id="@+id/bottomPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal"
android:background="#80000000"
android:padding="16dp"
android:gravity="center">
<!-- Кнопка назад -->
<Button
android:id="@+id/btnBack"
android:layout_width="80dp"
android:layout_height="80dp"
android:text="◀"
android:textSize="24sp"
android:textColor="#FFFFFF"
android:background="@drawable/circle_button_background"
android:layout_marginEnd="24dp" />
<!-- Кнопка захвата -->
<Button
android:id="@+id/btnCapture"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="📷"
android:textSize="32sp"
android:textColor="#FFFFFF"
android:background="@drawable/circle_button_background"
android:layout_marginHorizontal="24dp" />
<!-- Кнопка переключения камеры -->
<Button
android:id="@+id/btnSwitchCamera"
android:layout_width="80dp"
android:layout_height="80dp"
android:text="🔄"
android:textSize="20sp"
android:textColor="#FFFFFF"
android:background="@drawable/circle_button_background"
android:layout_marginStart="24dp" />
</LinearLayout>
<!-- Индикатор загрузки (скрыт по умолчанию) -->
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone" />
</RelativeLayout>

View File

@@ -0,0 +1,247 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:background="#F5F5F5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Заголовок приложения -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="GodEye Legacy"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#2196F3"
android:gravity="center"
android:layout_marginBottom="24dp" />
<!-- Карточка статуса -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Статус системы"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="❌ Сервис не подключен"
android:textSize="14sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvConnectionStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚪ Не подключено"
android:textSize="14sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvPermissions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚠️ Требуются разрешения"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Карточка информации об устройстве -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Информация об устройстве"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/tvDeviceInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📱 Загрузка..."
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Карточка с кнопками управления -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Управление"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="16dp" />
<!-- Кнопки подключения -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<Button
android:id="@+id/btnConnect"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Подключиться"
android:backgroundTint="#4CAF50"
android:textColor="#FFFFFF"
android:layout_marginEnd="8dp"
android:enabled="false" />
<Button
android:id="@+id/btnDisconnect"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Отключиться"
android:backgroundTint="#F44336"
android:textColor="#FFFFFF"
android:layout_marginStart="8dp" />
</LinearLayout>
<!-- Кнопки функций -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnCamera"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="📹 Камера"
android:backgroundTint="#2196F3"
android:textColor="#FFFFFF"
android:layout_marginEnd="8dp"
android:enabled="false" />
<Button
android:id="@+id/btnSettings"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="⚙️ Настройки"
android:backgroundTint="#9C27B0"
android:textColor="#FFFFFF"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Карточка с логами (для отладки) -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Информация для разработчика"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="• Версия для Android 9+"
android:textSize="12sp"
android:textColor="#666666"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="• Использует классические Views"
android:textSize="12sp"
android:textColor="#666666"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="• Совместимость с legacy устройствами"
android:textSize="12sp"
android:textColor="#666666" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

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"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Оригинальные цвета Material Design -->
<color name="purple_200">#FFBB86FC</color> <color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color> <color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color> <color name="purple_700">#FF3700B3</color>
@@ -7,4 +8,67 @@
<color name="teal_700">#FF018786</color> <color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</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> </resources>

View File

@@ -1,80 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">GodEye Signal Center</string> <string name="app_name">GodEye Signal Center</string>
<string name="connection_status">Статус подключения</string>
<!-- Главный экран --> <string name="connect">Подключиться</string>
<string name="device_id_label">ID устройства:</string> <string name="disconnect">Отключиться</string>
<string name="server_url_label">URL сервера:</string> <string name="server_url">URL сервера</string>
<string name="server_url_hint">http://192.168.1.100:3001</string> <string name="device_info">Информация об устройстве</string>
<string name="connection_status_label">Статус подключения:</string>
<string name="connect_button">Подключиться</string>
<string name="disconnect_button">Отключиться</string>
<string name="active_sessions_label">Активные сессии:</string>
<string name="no_active_sessions">Нет активных сессий</string>
<!-- Статусы подключения -->
<string name="status_disconnected">Отключено</string>
<string name="status_connecting">Подключение...</string>
<string name="status_connected">Подключено</string>
<string name="status_error">Ошибка подключения</string>
<string name="status_reconnecting">Переподключение...</string>
<!-- Диалог запроса камеры -->
<string name="camera_request_title">Запрос доступа к камере</string> <string name="camera_request_title">Запрос доступа к камере</string>
<string name="camera_request_message">Оператор %1$s запрашивает доступ к камере %2$s</string> <string name="allow">Разрешить</string>
<string name="session_id_label">ID сессии: %1$s</string> <string name="deny">Отклонить</string>
<string name="allow_button">Разрешить</string> <string name="active_sessions">Активные сессии</string>
<string name="deny_button">Отклонить</string> <string name="no_sessions">Нет активных сессий</string>
<string name="remember_choice">Запомнить для этого оператора</string> <string name="waiting_for_requests">Ожидание запросов операторов</string>
<string name="device_ready">Устройство готово к приему сессий</string>
<!-- Типы камер --> <string name="permissions_required">Требуются разрешения</string>
<string name="camera_type_back">Основная</string> <string name="grant_permissions">Предоставить разрешения</string>
<string name="camera_type_front">Фронтальная</string>
<string name="camera_type_wide">Широкоугольная</string>
<string name="camera_type_telephoto">Телеобъектив</string>
<!-- Сессии -->
<string name="session_operator_label">Оператор:</string>
<string name="session_camera_label">Камера:</string>
<string name="session_duration_label">Длительность:</string>
<string name="session_webrtc_status">WebRTC: %1$s</string>
<string name="webrtc_connected">Подключено</string>
<string name="webrtc_disconnected">Отключено</string>
<string name="end_session_button">Завершить</string>
<!-- Ошибки -->
<string name="error_network">Ошибка сети</string>
<string name="error_camera_permission">Нет разрешения на камеру</string>
<string name="error_audio_permission">Нет разрешения на микрофон</string>
<string name="error_camera_not_available">Камера недоступна</string>
<string name="error_webrtc_connection_failed">Ошибка WebRTC соединения</string>
<string name="error_socket">Ошибка WebSocket: %1$s</string>
<string name="error_camera">Ошибка камеры: %1$s</string>
<string name="error_unknown">Неизвестная ошибка</string>
<!-- Разрешения -->
<string name="permissions_required_title">Необходимы разрешения</string>
<string name="permissions_required_message">Для работы приложения необходимы разрешения на камеру, микрофон и уведомления</string>
<string name="grant_permissions_button">Предоставить разрешения</string>
<string name="permissions_denied_message">Без разрешений приложение не может работать</string>
<!-- Уведомления -->
<string name="notification_service_title">GodEye Signal Center</string>
<string name="notification_service_connected">Подключено к серверу</string>
<string name="notification_service_disconnected">Отключено от сервера</string>
<string name="notification_camera_title">GodEye Camera</string>
<string name="notification_camera_active">Активных сессий: %1$d</string>
<string name="notification_camera_ready">Камера готова к работе</string>
<!-- Общие -->
<string name="ok">OK</string>
<string name="cancel">Отмена</string>
<string name="settings">Настройки</string>
<string name="loading">Загрузка...</string>
<string name="retry">Повторить</string>
<!-- Время -->
<string name="duration_format">%1$02d:%2$02d:%3$02d</string>
<string name="time_seconds">сек</string>
<string name="time_minutes">мин</string>
<string name="time_hours">ч</string>
</resources> </resources>

View File

@@ -1,5 +1,35 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<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>
<style name="Theme.GodEye" parent="android:Theme.Material.Light.NoActionBar" /> <!-- Фон приложения -->
<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">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> </resources>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">192.168.219.108</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system"/>
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -0,0 +1,221 @@
package com.example.godeye.ui.components
import androidx.compose.animation.*
import androidx.compose.animation.core.*
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.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.example.godeye.ui.theme.GodEyeColors
@Composable
fun AnimatedFloatingPanel(
visible: Boolean,
modifier: Modifier = Modifier,
slideDirection: SlideDirection = SlideDirection.FromBottom,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
) { fullHeight ->
when (slideDirection) {
SlideDirection.FromTop -> -fullHeight
SlideDirection.FromBottom -> fullHeight
}
} + fadeIn(animationSpec = tween(300)),
exit = slideOutVertically(
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium
)
) { fullHeight ->
when (slideDirection) {
SlideDirection.FromTop -> -fullHeight
SlideDirection.FromBottom -> fullHeight
}
} + fadeOut(animationSpec = tween(200)),
modifier = modifier
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.BlackSoft.copy(alpha = 0.9f)
),
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
content()
}
}
}
@Composable
fun PulsingRecordIndicator(
isRecording: Boolean,
modifier: Modifier = Modifier
) {
if (isRecording) {
val infiniteTransition = rememberInfiniteTransition(label = "recording")
val scale by infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.size(12.dp)
.scale(scale)
.background(
GodEyeColors.RecordRed,
RoundedCornerShape(6.dp)
)
)
Text(
text = "REC",
color = GodEyeColors.RecordRed,
style = MaterialTheme.typography.labelSmall
)
}
}
}
@Composable
fun AnimatedControlButton(
onClick: () -> Unit,
icon: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
isActive: Boolean = false,
activeColor: Color = GodEyeColors.NavyLight,
size: ButtonSize = ButtonSize.Medium
) {
val scale by animateFloatAsState(
targetValue = if (isActive) 1.1f else 1.0f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
label = "button_scale"
)
val backgroundColor by animateColorAsState(
targetValue = if (isActive) activeColor else GodEyeColors.BlackSoft.copy(alpha = 0.8f),
animationSpec = tween(300),
label = "button_color"
)
val buttonSize = when (size) {
ButtonSize.Small -> 40.dp
ButtonSize.Medium -> 56.dp
ButtonSize.Large -> 72.dp
}
IconButton(
onClick = onClick,
modifier = modifier
.size(buttonSize)
.scale(scale)
.background(
backgroundColor,
RoundedCornerShape(buttonSize / 2)
)
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = if (isActive) Color.White else GodEyeColors.IvoryPure,
modifier = Modifier.size(buttonSize * 0.4f)
)
}
}
@Composable
fun SlideInErrorSnackbar(
error: String?,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
AnimatedVisibility(
visible = error != null,
enter = slideInVertically(
initialOffsetY = { -it },
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
) + fadeIn(),
exit = slideOutVertically(
targetOffsetY = { -it },
animationSpec = tween(300)
) + fadeOut(),
modifier = modifier
) {
if (error != null) {
Card(
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.RecordRed
),
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = error,
color = Color.White,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f)
)
IconButton(onClick = onDismiss) {
Icon(
Icons.Default.Close,
contentDescription = "Закрыть",
tint = Color.White
)
}
}
}
LaunchedEffect(error) {
kotlinx.coroutines.delay(5000)
onDismiss()
}
}
}
}
enum class SlideDirection {
FromTop,
FromBottom
}
enum class ButtonSize {
Small,
Medium,
Large
}

View File

@@ -0,0 +1,895 @@
package com.example.godeye.ui.components
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.godeye.models.*
import com.example.godeye.ui.theme.GodEyeColors
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ConnectionStatusIndicator(connectionState: ConnectionState) {
val infiniteTransition = rememberInfiniteTransition(label = "connection_indicator")
val animatedColor by infiniteTransition.animateColor(
initialValue = when (connectionState) {
ConnectionState.CONNECTED -> GodEyeColors.SuccessGreen
ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> GodEyeColors.WarningAmber
ConnectionState.ERROR -> GodEyeColors.RecordRed
ConnectionState.DISCONNECTED -> GodEyeColors.IvorySoft
},
targetValue = when (connectionState) {
ConnectionState.CONNECTED -> GodEyeColors.SuccessGreen.copy(alpha = 0.7f)
ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> GodEyeColors.WarningAmber.copy(alpha = 0.7f)
ConnectionState.ERROR -> GodEyeColors.RecordRed.copy(alpha = 0.7f)
ConnectionState.DISCONNECTED -> GodEyeColors.IvorySoft.copy(alpha = 0.7f)
},
animationSpec = infiniteRepeatable(
animation = tween(1500, easing = EaseInOut),
repeatMode = RepeatMode.Reverse
),
label = "indicator_color"
)
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = if (connectionState in listOf(ConnectionState.CONNECTING, ConnectionState.RECONNECTING)) 1.2f else 1f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = EaseInOut),
repeatMode = RepeatMode.Reverse
),
label = "indicator_scale"
)
Box(
modifier = Modifier
.size(16.dp)
.scale(scale)
.background(
brush = Brush.radialGradient(
colors = listOf(
animatedColor,
animatedColor.copy(alpha = 0.3f)
)
),
shape = RoundedCornerShape(50)
)
)
}
@Composable
fun ServerConfigurationPrompt(onSettingsClick: () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.1f)
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
Icons.Default.Warning,
contentDescription = null,
tint = GodEyeColors.WarningAmber,
modifier = Modifier.size(32.dp)
)
Text(
text = "Сервер не настроен",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
Text(
text = "Откройте настройки для выбора сервера",
style = MaterialTheme.typography.bodyMedium,
color = GodEyeColors.IvorySoft
)
Button(
onClick = onSettingsClick,
colors = ButtonDefaults.buttonColors(
containerColor = GodEyeColors.NavyLight
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
) {
Text("Открыть настройки")
}
}
}
}
@Composable
fun ServerInfoDisplay(
serverUrl: String,
onSettingsClick: () -> Unit,
onUpdateUrl: (String) -> Unit,
connectionState: ConnectionState,
isLoading: Boolean
) {
OutlinedTextField(
value = serverUrl,
onValueChange = onUpdateUrl,
label = {
Text(
"Server URL",
color = GodEyeColors.NavyLight
)
},
placeholder = {
Text(
"http://192.168.1.100:3001",
color = GodEyeColors.IvorySoft.copy(alpha = 0.6f)
)
},
modifier = Modifier.fillMaxWidth(),
enabled = connectionState != ConnectionState.CONNECTED && !isLoading,
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = GodEyeColors.NavyLight,
unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f),
focusedTextColor = GodEyeColors.IvoryPure,
unfocusedTextColor = GodEyeColors.IvorySoft
),
shape = RoundedCornerShape(12.dp),
trailingIcon = {
IconButton(onClick = onSettingsClick) {
Icon(
Icons.Default.Settings,
contentDescription = "Настройки",
tint = GodEyeColors.NavyLight
)
}
}
)
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ConnectionControls(
connectionState: ConnectionState,
isLoading: Boolean,
serverUrl: String,
onConnect: () -> Unit,
onDisconnect: () -> Unit,
onSettings: () -> Unit
) {
AnimatedContent(
targetState = connectionState,
transitionSpec = {
slideInHorizontally(
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
) togetherWith slideOutHorizontally(
animationSpec = tween(300)
)
},
label = "connection_controls"
) { state ->
when (state) {
ConnectionState.CONNECTED -> {
Button(
onClick = onDisconnect,
enabled = !isLoading,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.8f)
),
shape = RoundedCornerShape(12.dp)
) {
Icon(Icons.Default.PowerOff, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(
"Отключиться от сервера",
color = GodEyeColors.IvoryPure
)
}
}
else -> {
Button(
onClick = {
if (serverUrl.isBlank()) onSettings() else onConnect()
},
enabled = !isLoading,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = GodEyeColors.NavyLight
),
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
color = GodEyeColors.IvoryPure
)
Text(
"Подключение...",
color = GodEyeColors.IvoryPure
)
}
} else {
Icon(Icons.Default.Link, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(
if (serverUrl.isBlank()) "Настроить сервер" else "Подключиться к серверу",
color = GodEyeColors.IvoryPure
)
}
}
}
}
}
}
@Composable
fun OperatorRequestCard(
cameraRequest: CameraRequest,
onAccept: () -> Unit,
onReject: () -> Unit
) {
AnimatedFloatingPanel(
visible = true,
slideDirection = SlideDirection.FromBottom
) {
Column(
modifier = Modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
Icons.Default.Videocam,
contentDescription = null,
tint = GodEyeColors.WarningAmber,
modifier = Modifier.size(32.dp)
)
Column {
Text(
text = "Запрос на подключение к камере",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = GodEyeColors.IvoryPure
)
Text(
text = "Оператор ${cameraRequest.operatorId}",
style = MaterialTheme.typography.bodyMedium,
color = GodEyeColors.NavyLight
)
}
}
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.BlackMedium.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
InfoRow("Тип камеры", cameraRequest.cameraType)
InfoRow("Session ID", cameraRequest.sessionId.take(12) + "...")
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onReject,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.8f)
),
shape = RoundedCornerShape(12.dp)
) {
Icon(Icons.Default.Close, contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text("Отклонить", color = GodEyeColors.IvoryPure)
}
Button(
onClick = onAccept,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = GodEyeColors.SuccessGreen
),
shape = RoundedCornerShape(12.dp)
) {
Icon(Icons.Default.Check, contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text("Разрешить", color = GodEyeColors.IvoryPure)
}
}
}
}
}
@Composable
fun ActiveSessionsCard(
activeSessions: Map<String, SessionInfo>,
onEndSession: (String) -> Unit
) {
AnimatedFloatingPanel(
visible = true,
slideDirection = SlideDirection.FromBottom
) {
Column(
modifier = Modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
Icons.Default.VideoCall,
contentDescription = null,
tint = GodEyeColors.RecordRed,
modifier = Modifier.size(32.dp)
)
Column {
Text(
text = "Активная трансляция",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = GodEyeColors.IvoryPure
)
Text(
text = "Активных сессий: ${activeSessions.size}",
style = MaterialTheme.typography.bodyMedium,
color = GodEyeColors.IvorySoft
)
}
}
activeSessions.forEach { (sessionId, sessionInfo) ->
SessionCard(
sessionId = sessionId,
sessionInfo = sessionInfo,
onEndSession = onEndSession
)
}
}
}
}
@Composable
fun SessionCard(
sessionId: String,
sessionInfo: SessionInfo,
onEndSession: (String) -> Unit
) {
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),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Оператор: ${sessionInfo.operatorId}",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
Text(
text = "Камера: ${sessionInfo.cameraType}",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.NavyLight
)
Text(
text = "Статус: ${sessionInfo.status}",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
}
Button(
onClick = { onEndSession(sessionId) },
colors = ButtonDefaults.buttonColors(
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.8f)
),
shape = RoundedCornerShape(8.dp)
) {
Text("Завершить", color = GodEyeColors.IvoryPure)
}
}
}
}
@Composable
fun SuccessStatusCard(
cameraRequest: CameraRequest?,
isStreaming: Boolean
) {
AnimatedFloatingPanel(
visible = true,
slideDirection = SlideDirection.FromBottom
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
tint = GodEyeColors.SuccessGreen,
modifier = Modifier.size(48.dp)
)
Text(
text = "Успешно подключено!",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
Text(
text = "Android устройство зарегистрировано на сервере",
style = MaterialTheme.typography.bodyMedium,
color = GodEyeColors.IvorySoft
)
Text(
text = when {
cameraRequest != null -> "📷 Получен запрос на подключение к камере"
isStreaming -> "🔴 Трансляция активна"
else -> "Ожидание запросов от операторов..."
},
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.NavyLight
)
}
}
}
@Composable
private fun InfoRow(label: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
Text(
text = value,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
}
}
/**
* Сворачиваемая плитка для главного экрана
*/
@Composable
fun CollapsibleTile(
title: String,
subtitle: String,
icon: androidx.compose.ui.graphics.vector.ImageVector,
expanded: Boolean,
onToggle: () -> Unit,
modifier: Modifier = Modifier,
statusColor: androidx.compose.ui.graphics.Color = GodEyeColors.IvorySoft,
content: @Composable () -> Unit
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.BlackSoft.copy(alpha = 0.8f)
),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column {
// Заголовок плитки (всегда видимый)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onToggle() }
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = statusColor,
modifier = Modifier.size(24.dp)
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = statusColor
)
}
Icon(
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (expanded) "Свернуть" else "Развернуть",
tint = GodEyeColors.IvorySoft,
modifier = Modifier.size(20.dp)
)
}
// Развернутое содержимое
AnimatedVisibility(
visible = expanded,
enter = expandVertically(
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
) + fadeIn(),
exit = shrinkVertically(
animationSpec = tween(300)
) + fadeOut()
) {
content()
}
}
}
}
/**
* Плитка-кнопка для действий
*/
@Composable
fun ActionTile(
title: String,
subtitle: String,
icon: androidx.compose.ui.graphics.vector.ImageVector,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
backgroundColor: androidx.compose.ui.graphics.Color = GodEyeColors.NavyMedium
) {
Card(
modifier = modifier
.fillMaxWidth()
.clickable(enabled = enabled) { onClick() },
colors = CardDefaults.cardColors(
containerColor = if (enabled) backgroundColor else GodEyeColors.BlackMedium.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = if (enabled) 6.dp else 2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (enabled) GodEyeColors.IvoryPure else GodEyeColors.IvorySoft.copy(alpha = 0.5f),
modifier = Modifier.size(32.dp)
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = if (enabled) GodEyeColors.IvoryPure else GodEyeColors.IvorySoft.copy(alpha = 0.5f)
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = if (enabled) GodEyeColors.IvorySoft else GodEyeColors.IvorySoft.copy(alpha = 0.3f)
)
}
if (enabled) {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = GodEyeColors.IvorySoft,
modifier = Modifier.size(20.dp)
)
}
}
}
}
/**
* Карточка активной сессии
*/
@Composable
fun ActiveSessionCard(
sessionInfo: SessionInfo,
onEndSession: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.SuccessGreen.copy(alpha = 0.1f)
),
shape = RoundedCornerShape(8.dp),
border = BorderStroke(1.dp, GodEyeColors.SuccessGreen.copy(alpha = 0.3f))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(
text = "Оператор: ${sessionInfo.operatorId.take(8)}...",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
Text(
text = "Камера: ${sessionInfo.cameraType}",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
Text(
text = "Статус: ${sessionInfo.status}",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.SuccessGreen
)
}
IconButton(
onClick = onEndSession,
colors = IconButtonDefaults.iconButtonColors(
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.2f)
)
) {
Icon(
Icons.Default.Stop,
contentDescription = "Завершить сессию",
tint = GodEyeColors.RecordRed,
modifier = Modifier.size(20.dp)
)
}
}
}
}
}
@Composable
fun CameraPreviewCard(
isStreaming: Boolean,
activeSessions: Map<String, SessionInfo>,
onStartStreaming: () -> Unit,
onStopStreaming: () -> Unit,
onSwitchCamera: (String) -> Unit
) {
var currentCamera by remember { mutableStateOf("back") }
Card(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.8f)
),
border = BorderStroke(
1.dp,
if (isStreaming) GodEyeColors.SuccessGreen else GodEyeColors.NavyLight
),
shape = RoundedCornerShape(16.dp)
) {
Box(modifier = Modifier.fillMaxSize()) {
// Фон предпросмотра
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(
GodEyeColors.BlackSoft,
GodEyeColors.NavyDark
)
)
),
contentAlignment = Alignment.Center
) {
if (isStreaming) {
// Анимированный индикатор стриминга
val infiniteTransition = rememberInfiniteTransition(label = "streaming")
val alpha by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
),
label = "streaming_alpha"
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.Videocam,
contentDescription = "Streaming",
tint = GodEyeColors.SuccessGreen.copy(alpha = alpha),
modifier = Modifier.size(48.dp)
)
Text(
text = "LIVE",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = GodEyeColors.SuccessGreen.copy(alpha = alpha)
)
Text(
text = "${activeSessions.size} активных сессий",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
}
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.VideocamOff,
contentDescription = "Not streaming",
tint = GodEyeColors.IvorySoft,
modifier = Modifier.size(48.dp)
)
Text(
text = "Камера готова",
style = MaterialTheme.typography.titleMedium,
color = GodEyeColors.IvoryPure
)
Text(
text = "Ожидание запроса от оператора",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
}
}
}
// Верхняя панель управления
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
.align(Alignment.TopCenter),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Индикатор качества
Card(
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.BlackPure.copy(alpha = 0.7f)
),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = "1280x720",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvoryPure
)
}
// Кнопка переключения камеры
IconButton(
onClick = {
currentCamera = if (currentCamera == "back") "front" else "back"
onSwitchCamera(currentCamera)
},
modifier = Modifier
.background(
GodEyeColors.BlackPure.copy(alpha = 0.7f),
RoundedCornerShape(8.dp)
)
) {
Icon(
Icons.Default.FlipCameraAndroid,
contentDescription = "Switch camera",
tint = GodEyeColors.IvoryPure
)
}
}
// Нижняя панель управления
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
.align(Alignment.BottomCenter),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
// Кнопка управления стримингом
if (isStreaming) {
Button(
onClick = onStopStreaming,
colors = ButtonDefaults.buttonColors(
containerColor = GodEyeColors.RecordRed
),
shape = RoundedCornerShape(12.dp)
) {
Icon(
Icons.Default.Stop,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Остановить")
}
} else {
Button(
onClick = onStartStreaming,
colors = ButtonDefaults.buttonColors(
containerColor = GodEyeColors.SuccessGreen
),
shape = RoundedCornerShape(12.dp)
) {
Icon(
Icons.Default.PlayArrow,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Запустить")
}
}
}
}
}
}

View File

@@ -0,0 +1,512 @@
package com.example.godeye.ui.components
import androidx.compose.animation.*
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.text.input.KeyboardType
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 = "Камера") {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
Icons.Default.CameraAlt,
contentDescription = null,
tint = GodEyeColors.NavyLight,
modifier = Modifier.size(24.dp)
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Предпочитаемая камера",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
Text(
text = "Камера по умолчанию для стриминга",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
}
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
onClick = { preferredCamera = "back" },
label = { Text("Основная") },
selected = preferredCamera == "back",
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = GodEyeColors.NavyLight,
selectedLabelColor = GodEyeColors.IvoryPure
)
)
FilterChip(
onClick = { preferredCamera = "front" },
label = { Text("Фронтальная") },
selected = preferredCamera == "front",
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = GodEyeColors.NavyLight,
selectedLabelColor = GodEyeColors.IvoryPure
)
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
Icons.Default.HighQuality,
contentDescription = null,
tint = GodEyeColors.NavyLight,
modifier = Modifier.size(24.dp)
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Качество видео",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
Text(
text = "Разрешение видео потока",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
}
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
onClick = { streamQuality = "480p" },
label = { Text("480p") },
selected = streamQuality == "480p",
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = GodEyeColors.NavyLight,
selectedLabelColor = GodEyeColors.IvoryPure
)
)
FilterChip(
onClick = { streamQuality = "720p" },
label = { Text("720p") },
selected = streamQuality == "720p",
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = GodEyeColors.NavyLight,
selectedLabelColor = GodEyeColors.IvoryPure
)
)
FilterChip(
onClick = { streamQuality = "1080p" },
label = { Text("1080p") },
selected = streamQuality == "1080p",
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = GodEyeColors.NavyLight,
selectedLabelColor = GodEyeColors.IvoryPure
)
)
}
}
}
}
}
// Секция "О приложении"
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
private 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
private 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
private 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

@@ -0,0 +1,337 @@
package com.example.godeye.ui.dialogs
import android.app.Dialog
import android.os.Bundle
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.Brush
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.fragment.app.DialogFragment
import com.example.godeye.models.CameraRequest
import com.example.godeye.ui.theme.GodEyeColors
import com.example.godeye.ui.theme.GodEyeTheme
/**
* CameraRequestDialog - диалог запроса доступа к камере согласно ТЗ
* Отображает информацию об операторе и запрашиваемой камере
*/
class CameraRequestDialog : DialogFragment() {
private var cameraRequest: CameraRequest? = null
private var onAccept: (() -> Unit)? = null
private var onReject: (() -> Unit)? = null
private var autoAccept: Boolean = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
dialog.setContentView(ComposeView(requireContext()).apply {
setContent {
GodEyeTheme {
CameraRequestDialogContent(
cameraRequest = cameraRequest,
autoAccept = autoAccept,
onAccept = {
onAccept?.invoke()
dismiss()
},
onReject = {
onReject?.invoke()
dismiss()
}
)
}
}
})
dialog.setCancelable(false) // Пользователь должен явно принять решение
return dialog
}
companion object {
fun newInstance(
request: CameraRequest,
autoAccept: Boolean = false,
onAccept: () -> Unit,
onReject: () -> Unit
): CameraRequestDialog {
return CameraRequestDialog().apply {
this.cameraRequest = request
this.autoAccept = autoAccept
this.onAccept = onAccept
this.onReject = onReject
}
}
}
}
@Composable
private fun CameraRequestDialogContent(
cameraRequest: CameraRequest?,
autoAccept: Boolean,
onAccept: () -> Unit,
onReject: () -> Unit
) {
if (cameraRequest == null) return
// Автоматическое принятие если включено
if (autoAccept) {
LaunchedEffect(Unit) {
kotlinx.coroutines.delay(500) // Небольшая задержка для показа диалога
onAccept()
}
}
Dialog(onDismissRequest = { /* Нельзя закрыть без выбора */ }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.BlackSoft
),
elevation = CardDefaults.cardElevation(defaultElevation = 16.dp)
) {
Column(
modifier = Modifier
.background(
Brush.verticalGradient(
colors = listOf(
GodEyeColors.NavyDark.copy(alpha = 0.3f),
GodEyeColors.BlackSoft
)
)
)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Иконка оператора
Box(
modifier = Modifier
.size(80.dp)
.background(
Brush.radialGradient(
colors = listOf(
GodEyeColors.NavyLight.copy(alpha = 0.3f),
GodEyeColors.NavyDark.copy(alpha = 0.1f)
)
),
RoundedCornerShape(50)
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(40.dp),
tint = GodEyeColors.NavyLight
)
}
// Заголовок
Text(
text = "Запрос доступа к камере",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
// Информация о запросе согласно ТЗ
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = GodEyeColors.BlackMedium.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
InfoRow(
label = "Оператор",
value = cameraRequest.operatorId,
icon = Icons.Default.Person
)
InfoRow(
label = "Камера",
value = getCameraDisplayName(cameraRequest.cameraType),
icon = Icons.Default.Videocam
)
InfoRow(
label = "Session ID",
value = cameraRequest.sessionId.take(12) + "...",
icon = Icons.Default.Key
)
}
}
// Описание запроса
Text(
text = "Оператор ${cameraRequest.operatorId} запрашивает доступ к камере ${getCameraDisplayName(cameraRequest.cameraType)}",
style = MaterialTheme.typography.bodyMedium,
color = GodEyeColors.IvorySoft,
lineHeight = MaterialTheme.typography.bodyMedium.lineHeight * 1.4
)
// Индикатор автоматического принятия
if (autoAccept) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
color = GodEyeColors.SuccessGreen
)
Text(
text = "Автоматическое принятие...",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.SuccessGreen
)
}
}
// Кнопки действий согласно ТЗ
if (!autoAccept) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Кнопка "Отклонить"
OutlinedButton(
onClick = onReject,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = GodEyeColors.RecordRed
),
border = ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.horizontalGradient(
colors = listOf(
GodEyeColors.RecordRed,
GodEyeColors.RecordRed.copy(alpha = 0.7f)
)
)
),
shape = RoundedCornerShape(12.dp)
) {
Icon(
Icons.Default.Close,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Отклонить")
}
// Кнопка "Разрешить"
Button(
onClick = onAccept,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = GodEyeColors.SuccessGreen
),
shape = RoundedCornerShape(12.dp)
) {
Icon(
Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
"Разрешить",
color = GodEyeColors.IvoryPure
)
}
}
}
// Опция "Запомнить для этого оператора" согласно ТЗ
if (!autoAccept) {
var rememberChoice by remember { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Checkbox(
checked = rememberChoice,
onCheckedChange = { rememberChoice = it },
colors = CheckboxDefaults.colors(
checkedColor = GodEyeColors.NavyLight,
uncheckedColor = GodEyeColors.IvorySoft
)
)
Text(
text = "Запомнить для этого оператора",
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
}
}
}
}
}
}
@Composable
private fun InfoRow(
label: String,
value: String,
icon: androidx.compose.ui.graphics.vector.ImageVector
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = GodEyeColors.NavyLight
)
Column {
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = GodEyeColors.IvorySoft
)
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = GodEyeColors.IvoryPure
)
}
}
}
/**
* Получение человекочитаемого названия камеры согласно ТЗ
*/
private fun getCameraDisplayName(cameraType: String): String {
return when (cameraType) {
"back" -> "Основная камера"
"front" -> "Фронтальная камера"
"ultra_wide" -> "Широкоугольная камера"
"telephoto" -> "Телеобъектив"
else -> "Камера ($cameraType)"
}
}

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.

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