Compare commits
2 Commits
9951d8367f
...
79256cd9fc
| Author | SHA1 | Date | |
|---|---|---|---|
| 79256cd9fc | |||
| b1de55d253 |
6
.idea/AndroidProjectSystem.xml
generated
Normal file
6
.idea/AndroidProjectSystem.xml
generated
Normal 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
26
.idea/appInsightsSettings.xml
generated
Normal 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
6
.idea/compiler.xml
generated
Normal 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
6
.idea/copilot.data.migration.agent.xml
generated
Normal 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
6
.idea/copilot.data.migration.ask.xml
generated
Normal 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>
|
||||||
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal 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
6
.idea/copilot.data.migration.edit.xml
generated
Normal 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
18
.idea/deploymentTargetSelector.xml
generated
Normal 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
13
.idea/deviceManager.xml
generated
Normal 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
19
.idea/gradle.xml
generated
Normal 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>
|
||||||
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
10
.idea/migrations.xml
generated
Normal 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
16
.idea/misc.xml
generated
Normal 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
17
.idea/runConfigurations.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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>
|
||||||
4
.kotlin/errors/errors-1759321347725.log
Normal file
4
.kotlin/errors/errors-1759321347725.log
Normal 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
|
||||||
|
|
||||||
4
.kotlin/errors/errors-1759400006923.log
Normal file
4
.kotlin/errors/errors-1759400006923.log
Normal 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
|
||||||
|
|
||||||
4
.kotlin/errors/errors-1759466115565.log
Normal file
4
.kotlin/errors/errors-1759466115565.log
Normal 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
|
||||||
|
|
||||||
4
.kotlin/errors/errors-1759530749084.log
Normal file
4
.kotlin/errors/errors-1759530749084.log
Normal 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
|
||||||
|
|
||||||
78
.kotlin/errors/errors-1759565614541.log
Normal file
78
.kotlin/errors/errors-1759565614541.log
Normal 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
|
||||||
|
|
||||||
|
|
||||||
78
.kotlin/errors/errors-1759565654456.log
Normal file
78
.kotlin/errors/errors-1759565654456.log
Normal 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
|
||||||
|
|
||||||
|
|
||||||
79
app/build-legacy.gradle.kts
Normal file
79
app/build-legacy.gradle.kts
Normal 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")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
87
app/build.gradle.kts.backup
Normal file
87
app/build.gradle.kts.backup
Normal 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") // Старая версия
|
||||||
|
}
|
||||||
60
app/src/main/AndroidManifest-legacy.xml
Normal file
60
app/src/main/AndroidManifest-legacy.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
68
app/src/main/AndroidManifest.xml.backup
Normal file
68
app/src/main/AndroidManifest.xml.backup
Normal 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>
|
||||||
94
app/src/main/java/com/example/godeye/GodEyeApplication.kt
Normal file
94
app/src/main/java/com/example/godeye/GodEyeApplication.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
292
app/src/main/java/com/example/godeye/LegacyCameraActivity.kt
Normal file
292
app/src/main/java/com/example/godeye/LegacyCameraActivity.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
245
app/src/main/java/com/example/godeye/LegacyMainActivity.kt
Normal file
245
app/src/main/java/com/example/godeye/LegacyMainActivity.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
470
app/src/main/java/com/example/godeye/MainViewModel.kt
Normal file
470
app/src/main/java/com/example/godeye/MainViewModel.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
304
app/src/main/java/com/example/godeye/managers/Camera2Manager.kt
Normal file
304
app/src/main/java/com/example/godeye/managers/Camera2Manager.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1237
app/src/main/java/com/example/godeye/managers/ConnectionManager.kt
Normal file
1237
app/src/main/java/com/example/godeye/managers/ConnectionManager.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 -> "Проверка состояния сети"
|
||||||
Logger.d("All required permissions: ${hasAllRequiredPermissions()}")
|
Manifest.permission.WAKE_LOCK -> "Предотвращение засыпания устройства"
|
||||||
Logger.d("Camera permissions: ${hasCameraPermissions()}")
|
Manifest.permission.FOREGROUND_SERVICE -> "Работа в фоновом режиме"
|
||||||
Logger.d("Critical permissions: ${hasCriticalPermissions()}")
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Результат запроса разрешений
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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?
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
308
app/src/main/java/com/example/godeye/managers/SocketManager.kt
Normal file
308
app/src/main/java/com/example/godeye/managers/SocketManager.kt
Normal 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
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/src/main/java/com/example/godeye/models/CameraModels.kt
Normal file
26
app/src/main/java/com/example/godeye/models/CameraModels.kt
Normal 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> // Фокусные расстояния для определения типа
|
||||||
|
)
|
||||||
15
app/src/main/java/com/example/godeye/models/DeviceInfo.kt
Normal file
15
app/src/main/java/com/example/godeye/models/DeviceInfo.kt
Normal 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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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?
|
||||||
|
)
|
||||||
51
app/src/main/java/com/example/godeye/models/SocketEvents.kt
Normal file
51
app/src/main/java/com/example/godeye/models/SocketEvents.kt
Normal 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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
310
app/src/main/java/com/example/godeye/network/SignalingClient.kt
Normal file
310
app/src/main/java/com/example/godeye/network/SignalingClient.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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("Завершить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
74
app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt
Normal file
74
app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
app/src/main/java/com/example/godeye/utils/ErrorHandler.kt
Normal file
14
app/src/main/java/com/example/godeye/utils/ErrorHandler.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("CameraExtensions", "Error getting cameras", e)
|
|
||||||
// Добавляем базовые камеры как fallback
|
|
||||||
cameras.add(Constants.CameraTypes.BACK)
|
|
||||||
cameras.add(Constants.CameraTypes.FRONT)
|
|
||||||
}
|
|
||||||
return cameras.distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CameraManager.getCameraIdForType(cameraType: String): String? {
|
// Метод 2: Через NetworkInterface (универсальный метод)
|
||||||
|
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||||
|
for (networkInterface in interfaces) {
|
||||||
|
// Пропускаем loopback и неактивные интерфейсы
|
||||||
|
if (networkInterface.isLoopback || !networkInterface.isUp) continue
|
||||||
|
|
||||||
|
val addresses = networkInterface.inetAddresses
|
||||||
|
for (address in addresses) {
|
||||||
|
// Пропускаем IPv6 и loopback адреса
|
||||||
|
if (!address.isLoopbackAddress && address is java.net.Inet4Address) {
|
||||||
|
val ip = address.hostAddress
|
||||||
|
if (ip != null && !ip.startsWith("127.")) {
|
||||||
|
Logger.step("IP_DETECTION", "Network interface IP detected: $ip on ${networkInterface.name}")
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод 3: Через ConnectivityManager (Android 6+)
|
||||||
|
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||||
|
connectivityManager?.let { cm ->
|
||||||
|
val activeNetwork = cm.activeNetwork
|
||||||
|
val networkCapabilities = cm.getNetworkCapabilities(activeNetwork)
|
||||||
|
if (networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true) {
|
||||||
|
Logger.step("IP_DETECTION", "Active network detected but couldn't extract IP")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("IP_DETECTION_ERROR", "Failed to detect local IP", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: возвращаем IP по умолчанию для вашей подсети
|
||||||
|
val fallbackIP = "192.168.219.108" // ИСПРАВЛЕНИЕ: Изменено на ваш сервер
|
||||||
|
Logger.step("IP_DETECTION_FALLBACK", "Using fallback IP: $fallbackIP")
|
||||||
|
return fallbackIP
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* НОВОЕ: Получение IP сервера из URL
|
||||||
|
*/
|
||||||
|
fun extractServerIP(serverUrl: String): String {
|
||||||
return try {
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
75
app/src/main/java/com/example/godeye/utils/Logger.kt
Normal file
75
app/src/main/java/com/example/godeye/utils/Logger.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
185
app/src/main/java/com/example/godeye/utils/PreferenceManager.kt
Normal file
185
app/src/main/java/com/example/godeye/utils/PreferenceManager.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/src/main/res/drawable/app_background.xml
Normal file
24
app/src/main/res/drawable/app_background.xml
Normal 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>
|
||||||
22
app/src/main/res/drawable/button_danger.xml
Normal file
22
app/src/main/res/drawable/button_danger.xml
Normal 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>
|
||||||
|
|
||||||
22
app/src/main/res/drawable/button_primary.xml
Normal file
22
app/src/main/res/drawable/button_primary.xml
Normal 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>
|
||||||
|
|
||||||
25
app/src/main/res/drawable/button_secondary.xml
Normal file
25
app/src/main/res/drawable/button_secondary.xml
Normal 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>
|
||||||
|
|
||||||
16
app/src/main/res/drawable/circle_button_background.xml
Normal file
16
app/src/main/res/drawable/circle_button_background.xml
Normal 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>
|
||||||
|
|
||||||
9
app/src/main/res/drawable/circle_indicator.xml
Normal file
9
app/src/main/res/drawable/circle_indicator.xml
Normal 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>
|
||||||
|
|
||||||
15
app/src/main/res/drawable/ic_camera_active.xml
Normal file
15
app/src/main/res/drawable/ic_camera_active.xml
Normal 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>
|
||||||
11
app/src/main/res/drawable/ic_camera_request.xml
Normal file
11
app/src/main/res/drawable/ic_camera_request.xml
Normal 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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
14
app/src/main/res/drawable/ic_recording_active.xml
Normal file
14
app/src/main/res/drawable/ic_recording_active.xml
Normal 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>
|
||||||
|
|
||||||
7
app/src/main/res/drawable/info_panel_background.xml
Normal file
7
app/src/main/res/drawable/info_panel_background.xml
Normal 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>
|
||||||
100
app/src/main/res/layout/activity_legacy_camera.xml
Normal file
100
app/src/main/res/layout/activity_legacy_camera.xml
Normal 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>
|
||||||
247
app/src/main/res/layout/activity_legacy_main.xml
Normal file
247
app/src/main/res/layout/activity_legacy_main.xml
Normal 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>
|
||||||
256
app/src/main/res/layout/activity_streaming_monitor.xml
Normal file
256
app/src/main/res/layout/activity_streaming_monitor.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
14
app/src/main/res/xml/network_security_config.xml
Normal file
14
app/src/main/res/xml/network_security_config.xml
Normal 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>
|
||||||
|
|
||||||
221
app/src/ui.disabled.backup/components/AnimatedComponents.kt
Normal file
221
app/src/ui.disabled.backup/components/AnimatedComponents.kt
Normal 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
|
||||||
|
}
|
||||||
895
app/src/ui.disabled.backup/components/MainScreenComponents.kt
Normal file
895
app/src/ui.disabled.backup/components/MainScreenComponents.kt
Normal 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("Запустить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
512
app/src/ui.disabled.backup/components/SettingsScreen.kt
Normal file
512
app/src/ui.disabled.backup/components/SettingsScreen.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
337
app/src/ui.disabled.backup/dialogs/CameraRequestDialog.kt
Normal file
337
app/src/ui.disabled.backup/dialogs/CameraRequestDialog.kt
Normal 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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
112
app/src/ui.disabled.backup/theme/GodEyeTheme.kt
Normal file
112
app/src/ui.disabled.backup/theme/GodEyeTheme.kt
Normal 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
266
build.log
Normal 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
6
build_output.log
Normal 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
Reference in New Issue
Block a user