From b1de55d253fd0f65c1bfbbb7e131fc78bd0163fe Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Sat, 4 Oct 2025 11:55:35 +0900 Subject: [PATCH] init commit --- .idea/AndroidProjectSystem.xml | 6 + .idea/appInsightsSettings.xml | 26 + .idea/compiler.xml | 6 + .idea/copilot.data.migration.agent.xml | 6 + .idea/copilot.data.migration.ask.xml | 6 + .idea/copilot.data.migration.ask2agent.xml | 6 + .idea/copilot.data.migration.edit.xml | 6 + .idea/deploymentTargetSelector.xml | 18 + .idea/deviceManager.xml | 13 + .idea/gradle.xml | 19 + .idea/inspectionProfiles/Project_Default.xml | 50 + .idea/migrations.xml | 10 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 17 + .idea/vcs.xml | 6 + .kotlin/errors/errors-1759321347725.log | 4 + .kotlin/errors/errors-1759400006923.log | 4 + .kotlin/errors/errors-1759466115565.log | 4 + .kotlin/errors/errors-1759530749084.log | 4 + app/build-legacy.gradle.kts | 73 + app/build.gradle.kts | 112 +- app/src/main/AndroidManifest-legacy.xml | 60 + app/src/main/AndroidManifest.xml | 47 +- .../com/example/godeye/GodEyeApplication.kt | 94 + .../example/godeye/LegacyCameraActivity.kt | 292 + .../com/example/godeye/LegacyMainActivity.kt | 240 + .../java/com/example/godeye/MainActivity.kt | 473 +- .../java/com/example/godeye/MainViewModel.kt | 500 + .../java/com/example/godeye/SettingsScreen.kt | 364 + .../example/godeye/camera/CameraManager.kt | 236 + .../com/example/godeye/camera/CameraScreen.kt | 315 + .../example/godeye/managers/Camera2Manager.kt | 303 + .../example/godeye/managers/CameraManager.kt | 245 - .../godeye/managers/PermissionManager.kt | 255 +- .../example/godeye/managers/SessionManager.kt | 172 +- .../example/godeye/managers/WebRTCManager.kt | 145 - .../com/example/godeye/models/CameraModels.kt | 26 + .../java/com/example/godeye/models/Models.kt | 191 +- .../com/example/godeye/models/SocketEvents.kt | 62 + .../example/godeye/services/CameraService.kt | 425 - .../example/godeye/services/SocketService.kt | 564 +- .../godeye/streaming/HLSStreamManager.kt | 272 + .../godeye/streaming/RTSPStreamManager.kt | 331 + .../godeye/streaming/UDPStreamManager.kt | 193 + .../streaming/UnifiedStreamingManager.kt | 355 + .../godeye/streaming/WebRTCStreamManager.kt | 218 + .../ui/components/AnimatedComponents.kt | 221 + .../ui/components/CameraRequestDialog.kt | 164 - .../ui/components/ConnectionStatusCard.kt | 146 - .../ui/components/MainScreenComponents.kt | 895 ++ .../godeye/ui/components/SessionsList.kt | 237 - .../godeye/ui/components/SettingsScreen.kt | 512 + .../godeye/ui/dialogs/CameraRequestDialog.kt | 337 + .../example/godeye/ui/screens/MainScreen.kt | 318 - .../java/com/example/godeye/ui/theme/Color.kt | 11 - .../example/godeye/ui/theme/GodEyeTheme.kt | 112 + .../java/com/example/godeye/ui/theme/Theme.kt | 58 - .../java/com/example/godeye/ui/theme/Type.kt | 34 - .../godeye/ui/viewmodels/MainViewModel.kt | 385 - .../com/example/godeye/utils/Constants.kt | 98 +- .../com/example/godeye/utils/ErrorHandler.kt | 146 + .../com/example/godeye/utils/Extensions.kt | 131 +- .../java/com/example/godeye/utils/Logger.kt | 64 + .../example/godeye/webrtc/WebRTCManager.kt | 646 ++ .../res/drawable/circle_button_background.xml | 16 + .../res/layout/activity_legacy_camera.xml | 100 + .../main/res/layout/activity_legacy_main.xml | 247 + app/src/main/res/values/strings.xml | 91 +- app/src/main/res/values/themes.xml | 6 +- .../main/res/xml/network_security_config.xml | 14 + install_to_lg_g6.sh | 87 + node_modules/.bin/mime | 1 - node_modules/.bin/nodemon | 1 - node_modules/.bin/nodetouch | 1 - node_modules/.bin/semver | 1 - node_modules/.package-lock.json | 1316 --- .../@socket.io/component-emitter/LICENSE | 24 - .../@socket.io/component-emitter/Readme.md | 79 - .../component-emitter/lib/cjs/index.d.ts | 179 - .../component-emitter/lib/cjs/index.js | 176 - .../component-emitter/lib/cjs/package.json | 4 - .../component-emitter/lib/esm/index.d.ts | 179 - .../component-emitter/lib/esm/index.js | 169 - .../component-emitter/lib/esm/package.json | 4 - .../@socket.io/component-emitter/package.json | 28 - node_modules/@types/cors/LICENSE | 21 - node_modules/@types/cors/README.md | 75 - node_modules/@types/cors/index.d.ts | 56 - node_modules/@types/cors/package.json | 38 - node_modules/@types/node/LICENSE | 21 - node_modules/@types/node/README.md | 15 - node_modules/@types/node/assert.d.ts | 1056 -- node_modules/@types/node/assert/strict.d.ts | 8 - node_modules/@types/node/async_hooks.d.ts | 623 -- node_modules/@types/node/buffer.buffer.d.ts | 463 - node_modules/@types/node/buffer.d.ts | 1930 ---- node_modules/@types/node/child_process.d.ts | 1453 --- node_modules/@types/node/cluster.d.ts | 579 -- .../@types/node/compatibility/iterators.d.ts | 21 - node_modules/@types/node/console.d.ts | 452 - node_modules/@types/node/constants.d.ts | 21 - node_modules/@types/node/crypto.d.ts | 4566 --------- node_modules/@types/node/dgram.d.ts | 599 -- .../@types/node/diagnostics_channel.d.ts | 578 -- node_modules/@types/node/dns.d.ts | 923 -- node_modules/@types/node/dns/promises.d.ts | 503 - node_modules/@types/node/domain.d.ts | 170 - node_modules/@types/node/events.d.ts | 930 -- node_modules/@types/node/fs.d.ts | 4473 --------- node_modules/@types/node/fs/promises.d.ts | 1299 --- node_modules/@types/node/globals.d.ts | 168 - .../@types/node/globals.typedarray.d.ts | 22 - node_modules/@types/node/http.d.ts | 2070 ---- node_modules/@types/node/http2.d.ts | 2630 ----- node_modules/@types/node/https.d.ts | 550 -- node_modules/@types/node/index.d.ts | 99 - node_modules/@types/node/inspector.d.ts | 253 - .../@types/node/inspector.generated.d.ts | 4052 -------- node_modules/@types/node/module.d.ts | 894 -- node_modules/@types/node/net.d.ts | 1053 -- node_modules/@types/node/os.d.ts | 496 - node_modules/@types/node/package.json | 155 - node_modules/@types/node/path.d.ts | 200 - node_modules/@types/node/perf_hooks.d.ts | 984 -- node_modules/@types/node/process.d.ts | 2073 ---- node_modules/@types/node/punycode.d.ts | 117 - node_modules/@types/node/querystring.d.ts | 152 - node_modules/@types/node/readline.d.ts | 594 -- .../@types/node/readline/promises.d.ts | 161 - node_modules/@types/node/repl.d.ts | 438 - node_modules/@types/node/sea.d.ts | 153 - node_modules/@types/node/sqlite.d.ts | 721 -- node_modules/@types/node/stream.d.ts | 1668 ---- .../@types/node/stream/consumers.d.ts | 38 - node_modules/@types/node/stream/promises.d.ts | 90 - node_modules/@types/node/stream/web.d.ts | 622 -- node_modules/@types/node/string_decoder.d.ts | 67 - node_modules/@types/node/test.d.ts | 2334 ----- node_modules/@types/node/timers.d.ts | 285 - node_modules/@types/node/timers/promises.d.ts | 108 - node_modules/@types/node/tls.d.ts | 1245 --- node_modules/@types/node/trace_events.d.ts | 197 - .../@types/node/ts5.6/buffer.buffer.d.ts | 460 - .../ts5.6/compatibility/float16array.d.ts | 71 - .../@types/node/ts5.6/globals.typedarray.d.ts | 20 - node_modules/@types/node/ts5.6/index.d.ts | 101 - .../ts5.7/compatibility/float16array.d.ts | 72 - node_modules/@types/node/ts5.7/index.d.ts | 101 - node_modules/@types/node/tty.d.ts | 208 - node_modules/@types/node/url.d.ts | 1028 -- node_modules/@types/node/util.d.ts | 2311 ----- node_modules/@types/node/v8.d.ts | 919 -- node_modules/@types/node/vm.d.ts | 1099 --- node_modules/@types/node/wasi.d.ts | 202 - .../node/web-globals/abortcontroller.d.ts | 34 - .../@types/node/web-globals/domexception.d.ts | 68 - .../@types/node/web-globals/events.d.ts | 94 - .../@types/node/web-globals/fetch.d.ts | 50 - .../@types/node/web-globals/navigator.d.ts | 25 - .../@types/node/web-globals/storage.d.ts | 24 - node_modules/@types/node/worker_threads.d.ts | 835 -- node_modules/@types/node/zlib.d.ts | 674 -- node_modules/accepts/HISTORY.md | 243 - node_modules/accepts/LICENSE | 23 - node_modules/accepts/README.md | 140 - node_modules/accepts/index.js | 238 - node_modules/accepts/package.json | 47 - node_modules/anymatch/LICENSE | 15 - node_modules/anymatch/README.md | 87 - node_modules/anymatch/index.d.ts | 20 - node_modules/anymatch/index.js | 104 - node_modules/anymatch/package.json | 48 - node_modules/array-flatten/LICENSE | 21 - node_modules/array-flatten/README.md | 43 - node_modules/array-flatten/array-flatten.js | 64 - node_modules/array-flatten/package.json | 39 - .../balanced-match/.github/FUNDING.yml | 2 - node_modules/balanced-match/LICENSE.md | 21 - node_modules/balanced-match/README.md | 97 - node_modules/balanced-match/index.js | 62 - node_modules/balanced-match/package.json | 48 - node_modules/base64id/CHANGELOG.md | 16 - node_modules/base64id/LICENSE | 22 - node_modules/base64id/README.md | 18 - node_modules/base64id/lib/base64id.js | 103 - node_modules/base64id/package.json | 13 - .../binary-extensions/binary-extensions.json | 263 - .../binary-extensions.json.d.ts | 3 - node_modules/binary-extensions/index.d.ts | 14 - node_modules/binary-extensions/index.js | 1 - node_modules/binary-extensions/license | 10 - node_modules/binary-extensions/package.json | 40 - node_modules/binary-extensions/readme.md | 25 - node_modules/body-parser/HISTORY.md | 672 -- node_modules/body-parser/LICENSE | 23 - node_modules/body-parser/README.md | 476 - node_modules/body-parser/SECURITY.md | 25 - node_modules/body-parser/index.js | 156 - node_modules/body-parser/lib/read.js | 205 - node_modules/body-parser/lib/types/json.js | 247 - node_modules/body-parser/lib/types/raw.js | 101 - node_modules/body-parser/lib/types/text.js | 121 - .../body-parser/lib/types/urlencoded.js | 307 - node_modules/body-parser/package.json | 56 - node_modules/brace-expansion/LICENSE | 21 - node_modules/brace-expansion/README.md | 129 - node_modules/brace-expansion/index.js | 201 - node_modules/brace-expansion/package.json | 50 - node_modules/braces/LICENSE | 21 - node_modules/braces/README.md | 586 -- node_modules/braces/index.js | 170 - node_modules/braces/lib/compile.js | 60 - node_modules/braces/lib/constants.js | 57 - node_modules/braces/lib/expand.js | 113 - node_modules/braces/lib/parse.js | 331 - node_modules/braces/lib/stringify.js | 32 - node_modules/braces/lib/utils.js | 122 - node_modules/braces/package.json | 77 - node_modules/bytes/History.md | 97 - node_modules/bytes/LICENSE | 23 - node_modules/bytes/Readme.md | 152 - node_modules/bytes/index.js | 170 - node_modules/bytes/package.json | 42 - .../call-bind-apply-helpers/.eslintrc | 17 - .../.github/FUNDING.yml | 12 - node_modules/call-bind-apply-helpers/.nycrc | 9 - .../call-bind-apply-helpers/CHANGELOG.md | 30 - node_modules/call-bind-apply-helpers/LICENSE | 21 - .../call-bind-apply-helpers/README.md | 62 - .../call-bind-apply-helpers/actualApply.d.ts | 1 - .../call-bind-apply-helpers/actualApply.js | 10 - .../call-bind-apply-helpers/applyBind.d.ts | 19 - .../call-bind-apply-helpers/applyBind.js | 10 - .../functionApply.d.ts | 1 - .../call-bind-apply-helpers/functionApply.js | 4 - .../call-bind-apply-helpers/functionCall.d.ts | 1 - .../call-bind-apply-helpers/functionCall.js | 4 - .../call-bind-apply-helpers/index.d.ts | 64 - node_modules/call-bind-apply-helpers/index.js | 15 - .../call-bind-apply-helpers/package.json | 85 - .../call-bind-apply-helpers/reflectApply.d.ts | 3 - .../call-bind-apply-helpers/reflectApply.js | 4 - .../call-bind-apply-helpers/test/index.js | 63 - .../call-bind-apply-helpers/tsconfig.json | 9 - node_modules/call-bound/.eslintrc | 13 - node_modules/call-bound/.github/FUNDING.yml | 12 - node_modules/call-bound/.nycrc | 9 - node_modules/call-bound/CHANGELOG.md | 42 - node_modules/call-bound/LICENSE | 21 - node_modules/call-bound/README.md | 53 - node_modules/call-bound/index.d.ts | 94 - node_modules/call-bound/index.js | 19 - node_modules/call-bound/package.json | 99 - node_modules/call-bound/test/index.js | 61 - node_modules/call-bound/tsconfig.json | 10 - node_modules/chokidar/LICENSE | 21 - node_modules/chokidar/README.md | 308 - node_modules/chokidar/index.js | 973 -- node_modules/chokidar/lib/constants.js | 66 - node_modules/chokidar/lib/fsevents-handler.js | 526 - node_modules/chokidar/lib/nodefs-handler.js | 654 -- node_modules/chokidar/package.json | 70 - node_modules/chokidar/types/index.d.ts | 192 - node_modules/concat-map/.travis.yml | 4 - node_modules/concat-map/LICENSE | 18 - node_modules/concat-map/README.markdown | 62 - node_modules/concat-map/example/map.js | 6 - node_modules/concat-map/index.js | 13 - node_modules/concat-map/package.json | 43 - node_modules/concat-map/test/map.js | 39 - node_modules/content-disposition/HISTORY.md | 60 - node_modules/content-disposition/LICENSE | 22 - node_modules/content-disposition/README.md | 142 - node_modules/content-disposition/index.js | 458 - node_modules/content-disposition/package.json | 44 - node_modules/content-type/HISTORY.md | 29 - node_modules/content-type/LICENSE | 22 - node_modules/content-type/README.md | 94 - node_modules/content-type/index.js | 225 - node_modules/content-type/package.json | 42 - node_modules/cookie-signature/.npmignore | 4 - node_modules/cookie-signature/History.md | 38 - node_modules/cookie-signature/Readme.md | 42 - node_modules/cookie-signature/index.js | 51 - node_modules/cookie-signature/package.json | 18 - node_modules/cookie/LICENSE | 24 - node_modules/cookie/README.md | 317 - node_modules/cookie/SECURITY.md | 25 - node_modules/cookie/index.js | 334 - node_modules/cookie/package.json | 44 - node_modules/cors/CONTRIBUTING.md | 33 - node_modules/cors/HISTORY.md | 58 - node_modules/cors/LICENSE | 22 - node_modules/cors/README.md | 243 - node_modules/cors/lib/index.js | 238 - node_modules/cors/package.json | 41 - node_modules/debug/.coveralls.yml | 1 - node_modules/debug/.eslintrc | 11 - node_modules/debug/.npmignore | 9 - node_modules/debug/.travis.yml | 14 - node_modules/debug/CHANGELOG.md | 362 - node_modules/debug/LICENSE | 19 - node_modules/debug/Makefile | 50 - node_modules/debug/README.md | 312 - node_modules/debug/component.json | 19 - node_modules/debug/karma.conf.js | 70 - node_modules/debug/node.js | 1 - node_modules/debug/package.json | 49 - node_modules/debug/src/browser.js | 185 - node_modules/debug/src/debug.js | 202 - node_modules/debug/src/index.js | 10 - node_modules/debug/src/inspector-log.js | 15 - node_modules/debug/src/node.js | 248 - node_modules/depd/History.md | 103 - node_modules/depd/LICENSE | 22 - node_modules/depd/Readme.md | 280 - node_modules/depd/index.js | 538 -- node_modules/depd/lib/browser/index.js | 77 - node_modules/depd/package.json | 45 - node_modules/destroy/LICENSE | 23 - node_modules/destroy/README.md | 63 - node_modules/destroy/index.js | 209 - node_modules/destroy/package.json | 48 - node_modules/dunder-proto/.eslintrc | 5 - node_modules/dunder-proto/.github/FUNDING.yml | 12 - node_modules/dunder-proto/.nycrc | 13 - node_modules/dunder-proto/CHANGELOG.md | 24 - node_modules/dunder-proto/LICENSE | 21 - node_modules/dunder-proto/README.md | 54 - node_modules/dunder-proto/get.d.ts | 5 - node_modules/dunder-proto/get.js | 30 - node_modules/dunder-proto/package.json | 76 - node_modules/dunder-proto/set.d.ts | 5 - node_modules/dunder-proto/set.js | 35 - node_modules/dunder-proto/test/get.js | 34 - node_modules/dunder-proto/test/index.js | 4 - node_modules/dunder-proto/test/set.js | 50 - node_modules/dunder-proto/tsconfig.json | 9 - node_modules/ee-first/LICENSE | 22 - node_modules/ee-first/README.md | 80 - node_modules/ee-first/index.js | 95 - node_modules/ee-first/package.json | 29 - node_modules/encodeurl/LICENSE | 22 - node_modules/encodeurl/README.md | 109 - node_modules/encodeurl/index.js | 60 - node_modules/encodeurl/package.json | 40 - node_modules/engine.io-parser/LICENSE | 22 - node_modules/engine.io-parser/Readme.md | 158 - .../engine.io-parser/build/cjs/commons.d.ts | 14 - .../engine.io-parser/build/cjs/commons.js | 19 - .../build/cjs/contrib/base64-arraybuffer.d.ts | 2 - .../build/cjs/contrib/base64-arraybuffer.js | 48 - .../build/cjs/decodePacket.browser.d.ts | 2 - .../build/cjs/decodePacket.browser.js | 66 - .../build/cjs/decodePacket.d.ts | 2 - .../build/cjs/decodePacket.js | 59 - .../build/cjs/encodePacket.browser.d.ts | 4 - .../build/cjs/encodePacket.browser.js | 72 - .../build/cjs/encodePacket.d.ts | 3 - .../build/cjs/encodePacket.js | 38 - .../engine.io-parser/build/cjs/index.d.ts | 9 - .../engine.io-parser/build/cjs/index.js | 164 - .../engine.io-parser/build/cjs/package.json | 8 - .../engine.io-parser/build/esm/commons.d.ts | 14 - .../engine.io-parser/build/esm/commons.js | 14 - .../build/esm/contrib/base64-arraybuffer.d.ts | 2 - .../build/esm/contrib/base64-arraybuffer.js | 43 - .../build/esm/decodePacket.browser.d.ts | 2 - .../build/esm/decodePacket.browser.js | 62 - .../build/esm/decodePacket.d.ts | 2 - .../build/esm/decodePacket.js | 55 - .../build/esm/encodePacket.browser.d.ts | 4 - .../build/esm/encodePacket.browser.js | 68 - .../build/esm/encodePacket.d.ts | 3 - .../build/esm/encodePacket.js | 33 - .../engine.io-parser/build/esm/index.d.ts | 9 - .../engine.io-parser/build/esm/index.js | 156 - .../engine.io-parser/build/esm/package.json | 8 - node_modules/engine.io-parser/package.json | 46 - node_modules/engine.io/LICENSE | 19 - node_modules/engine.io/README.md | 603 -- .../engine.io/build/contrib/types.cookie.d.ts | 113 - .../engine.io/build/contrib/types.cookie.js | 6 - node_modules/engine.io/build/engine.io.d.ts | 26 - node_modules/engine.io/build/engine.io.js | 54 - .../engine.io/build/parser-v3/index.d.ts | 94 - .../engine.io/build/parser-v3/index.js | 424 - .../engine.io/build/parser-v3/utf8.d.ts | 14 - .../engine.io/build/parser-v3/utf8.js | 187 - node_modules/engine.io/build/server.d.ts | 267 - node_modules/engine.io/build/server.js | 786 -- node_modules/engine.io/build/socket.d.ts | 180 - node_modules/engine.io/build/socket.js | 460 - node_modules/engine.io/build/transport.d.ts | 124 - node_modules/engine.io/build/transport.js | 117 - .../engine.io/build/transports-uws/index.d.ts | 7 - .../engine.io/build/transports-uws/index.js | 8 - .../build/transports-uws/polling.d.ts | 99 - .../engine.io/build/transports-uws/polling.js | 364 - .../build/transports-uws/websocket.d.ts | 32 - .../build/transports-uws/websocket.js | 73 - .../engine.io/build/transports/index.d.ts | 16 - .../engine.io/build/transports/index.js | 23 - .../build/transports/polling-jsonp.d.ts | 12 - .../build/transports/polling-jsonp.js | 41 - .../engine.io/build/transports/polling.d.ts | 87 - .../engine.io/build/transports/polling.js | 332 - .../engine.io/build/transports/websocket.d.ts | 32 - .../engine.io/build/transports/websocket.js | 94 - .../build/transports/webtransport.d.ts | 12 - .../build/transports/webtransport.js | 63 - node_modules/engine.io/build/userver.d.ts | 42 - node_modules/engine.io/build/userver.js | 279 - .../engine.io/node_modules/cookie/LICENSE | 24 - .../engine.io/node_modules/cookie/README.md | 317 - .../engine.io/node_modules/cookie/SECURITY.md | 25 - .../engine.io/node_modules/cookie/index.js | 335 - .../node_modules/cookie/package.json | 44 - .../engine.io/node_modules/debug/LICENSE | 20 - .../engine.io/node_modules/debug/README.md | 481 - .../engine.io/node_modules/debug/package.json | 60 - .../node_modules/debug/src/browser.js | 271 - .../node_modules/debug/src/common.js | 274 - .../engine.io/node_modules/debug/src/index.js | 10 - .../engine.io/node_modules/debug/src/node.js | 263 - .../engine.io/node_modules/ms/index.js | 162 - .../engine.io/node_modules/ms/license.md | 21 - .../engine.io/node_modules/ms/package.json | 38 - .../engine.io/node_modules/ms/readme.md | 59 - node_modules/engine.io/package.json | 70 - node_modules/engine.io/wrapper.mjs | 10 - node_modules/es-define-property/.eslintrc | 13 - .../es-define-property/.github/FUNDING.yml | 12 - node_modules/es-define-property/.nycrc | 9 - node_modules/es-define-property/CHANGELOG.md | 29 - node_modules/es-define-property/LICENSE | 21 - node_modules/es-define-property/README.md | 49 - node_modules/es-define-property/index.d.ts | 3 - node_modules/es-define-property/index.js | 14 - node_modules/es-define-property/package.json | 81 - node_modules/es-define-property/test/index.js | 56 - node_modules/es-define-property/tsconfig.json | 10 - node_modules/es-errors/.eslintrc | 5 - node_modules/es-errors/.github/FUNDING.yml | 12 - node_modules/es-errors/CHANGELOG.md | 40 - node_modules/es-errors/LICENSE | 21 - node_modules/es-errors/README.md | 55 - node_modules/es-errors/eval.d.ts | 3 - node_modules/es-errors/eval.js | 4 - node_modules/es-errors/index.d.ts | 3 - node_modules/es-errors/index.js | 4 - node_modules/es-errors/package.json | 80 - node_modules/es-errors/range.d.ts | 3 - node_modules/es-errors/range.js | 4 - node_modules/es-errors/ref.d.ts | 3 - node_modules/es-errors/ref.js | 4 - node_modules/es-errors/syntax.d.ts | 3 - node_modules/es-errors/syntax.js | 4 - node_modules/es-errors/test/index.js | 19 - node_modules/es-errors/tsconfig.json | 49 - node_modules/es-errors/type.d.ts | 3 - node_modules/es-errors/type.js | 4 - node_modules/es-errors/uri.d.ts | 3 - node_modules/es-errors/uri.js | 4 - node_modules/es-object-atoms/.eslintrc | 16 - .../es-object-atoms/.github/FUNDING.yml | 12 - node_modules/es-object-atoms/CHANGELOG.md | 37 - node_modules/es-object-atoms/LICENSE | 21 - node_modules/es-object-atoms/README.md | 63 - .../RequireObjectCoercible.d.ts | 3 - .../es-object-atoms/RequireObjectCoercible.js | 11 - node_modules/es-object-atoms/ToObject.d.ts | 7 - node_modules/es-object-atoms/ToObject.js | 10 - node_modules/es-object-atoms/index.d.ts | 3 - node_modules/es-object-atoms/index.js | 4 - node_modules/es-object-atoms/isObject.d.ts | 3 - node_modules/es-object-atoms/isObject.js | 6 - node_modules/es-object-atoms/package.json | 80 - node_modules/es-object-atoms/test/index.js | 38 - node_modules/es-object-atoms/tsconfig.json | 6 - node_modules/escape-html/LICENSE | 24 - node_modules/escape-html/Readme.md | 43 - node_modules/escape-html/index.js | 78 - node_modules/escape-html/package.json | 24 - node_modules/etag/HISTORY.md | 83 - node_modules/etag/LICENSE | 22 - node_modules/etag/README.md | 159 - node_modules/etag/index.js | 131 - node_modules/etag/package.json | 47 - node_modules/express/History.md | 3656 ------- node_modules/express/LICENSE | 24 - node_modules/express/Readme.md | 260 - node_modules/express/index.js | 11 - node_modules/express/lib/application.js | 661 -- node_modules/express/lib/express.js | 116 - node_modules/express/lib/middleware/init.js | 43 - node_modules/express/lib/middleware/query.js | 47 - node_modules/express/lib/request.js | 525 - node_modules/express/lib/response.js | 1179 --- node_modules/express/lib/router/index.js | 673 -- node_modules/express/lib/router/layer.js | 181 - node_modules/express/lib/router/route.js | 230 - node_modules/express/lib/utils.js | 303 - node_modules/express/lib/view.js | 182 - node_modules/express/package.json | 102 - node_modules/fill-range/LICENSE | 21 - node_modules/fill-range/README.md | 237 - node_modules/fill-range/index.js | 248 - node_modules/fill-range/package.json | 74 - node_modules/finalhandler/HISTORY.md | 210 - node_modules/finalhandler/LICENSE | 22 - node_modules/finalhandler/README.md | 147 - node_modules/finalhandler/SECURITY.md | 25 - node_modules/finalhandler/index.js | 341 - node_modules/finalhandler/package.json | 47 - node_modules/forwarded/HISTORY.md | 21 - node_modules/forwarded/LICENSE | 22 - node_modules/forwarded/README.md | 57 - node_modules/forwarded/index.js | 90 - node_modules/forwarded/package.json | 45 - node_modules/fresh/HISTORY.md | 70 - node_modules/fresh/LICENSE | 23 - node_modules/fresh/README.md | 119 - node_modules/fresh/index.js | 137 - node_modules/fresh/package.json | 46 - node_modules/function-bind/.eslintrc | 21 - .../function-bind/.github/FUNDING.yml | 12 - .../function-bind/.github/SECURITY.md | 3 - node_modules/function-bind/.nycrc | 13 - node_modules/function-bind/CHANGELOG.md | 136 - node_modules/function-bind/LICENSE | 20 - node_modules/function-bind/README.md | 46 - node_modules/function-bind/implementation.js | 84 - node_modules/function-bind/index.js | 5 - node_modules/function-bind/package.json | 87 - node_modules/function-bind/test/.eslintrc | 9 - node_modules/function-bind/test/index.js | 252 - node_modules/get-intrinsic/.eslintrc | 42 - .../get-intrinsic/.github/FUNDING.yml | 12 - node_modules/get-intrinsic/.nycrc | 9 - node_modules/get-intrinsic/CHANGELOG.md | 186 - node_modules/get-intrinsic/LICENSE | 21 - node_modules/get-intrinsic/README.md | 71 - node_modules/get-intrinsic/index.js | 378 - node_modules/get-intrinsic/package.json | 97 - .../get-intrinsic/test/GetIntrinsic.js | 274 - node_modules/get-proto/.eslintrc | 10 - node_modules/get-proto/.github/FUNDING.yml | 12 - node_modules/get-proto/.nycrc | 9 - node_modules/get-proto/CHANGELOG.md | 21 - node_modules/get-proto/LICENSE | 21 - .../get-proto/Object.getPrototypeOf.d.ts | 5 - .../get-proto/Object.getPrototypeOf.js | 6 - node_modules/get-proto/README.md | 50 - .../get-proto/Reflect.getPrototypeOf.d.ts | 3 - .../get-proto/Reflect.getPrototypeOf.js | 4 - node_modules/get-proto/index.d.ts | 5 - node_modules/get-proto/index.js | 27 - node_modules/get-proto/package.json | 81 - node_modules/get-proto/test/index.js | 68 - node_modules/get-proto/tsconfig.json | 9 - node_modules/glob-parent/CHANGELOG.md | 110 - node_modules/glob-parent/LICENSE | 15 - node_modules/glob-parent/README.md | 137 - node_modules/glob-parent/index.js | 42 - node_modules/glob-parent/package.json | 48 - node_modules/gopd/.eslintrc | 16 - node_modules/gopd/.github/FUNDING.yml | 12 - node_modules/gopd/CHANGELOG.md | 45 - node_modules/gopd/LICENSE | 21 - node_modules/gopd/README.md | 40 - node_modules/gopd/gOPD.d.ts | 1 - node_modules/gopd/gOPD.js | 4 - node_modules/gopd/index.d.ts | 5 - node_modules/gopd/index.js | 15 - node_modules/gopd/package.json | 77 - node_modules/gopd/test/index.js | 36 - node_modules/gopd/tsconfig.json | 9 - node_modules/has-flag/index.js | 8 - node_modules/has-flag/license | 9 - node_modules/has-flag/package.json | 44 - node_modules/has-flag/readme.md | 70 - node_modules/has-symbols/.eslintrc | 11 - node_modules/has-symbols/.github/FUNDING.yml | 12 - node_modules/has-symbols/.nycrc | 9 - node_modules/has-symbols/CHANGELOG.md | 91 - node_modules/has-symbols/LICENSE | 21 - node_modules/has-symbols/README.md | 46 - node_modules/has-symbols/index.d.ts | 3 - node_modules/has-symbols/index.js | 14 - node_modules/has-symbols/package.json | 111 - node_modules/has-symbols/shams.d.ts | 3 - node_modules/has-symbols/shams.js | 45 - node_modules/has-symbols/test/index.js | 22 - .../has-symbols/test/shams/core-js.js | 29 - .../test/shams/get-own-property-symbols.js | 29 - node_modules/has-symbols/test/tests.js | 58 - node_modules/has-symbols/tsconfig.json | 10 - node_modules/hasown/.eslintrc | 5 - node_modules/hasown/.github/FUNDING.yml | 12 - node_modules/hasown/.nycrc | 13 - node_modules/hasown/CHANGELOG.md | 40 - node_modules/hasown/LICENSE | 21 - node_modules/hasown/README.md | 40 - node_modules/hasown/index.d.ts | 3 - node_modules/hasown/index.js | 8 - node_modules/hasown/package.json | 92 - node_modules/hasown/tsconfig.json | 6 - node_modules/http-errors/HISTORY.md | 180 - node_modules/http-errors/LICENSE | 23 - node_modules/http-errors/README.md | 169 - node_modules/http-errors/index.js | 289 - node_modules/http-errors/package.json | 50 - node_modules/iconv-lite/Changelog.md | 162 - node_modules/iconv-lite/LICENSE | 21 - node_modules/iconv-lite/README.md | 156 - .../iconv-lite/encodings/dbcs-codec.js | 555 -- .../iconv-lite/encodings/dbcs-data.js | 176 - node_modules/iconv-lite/encodings/index.js | 22 - node_modules/iconv-lite/encodings/internal.js | 188 - .../iconv-lite/encodings/sbcs-codec.js | 72 - .../encodings/sbcs-data-generated.js | 451 - .../iconv-lite/encodings/sbcs-data.js | 174 - .../encodings/tables/big5-added.json | 122 - .../iconv-lite/encodings/tables/cp936.json | 264 - .../iconv-lite/encodings/tables/cp949.json | 273 - .../iconv-lite/encodings/tables/cp950.json | 177 - .../iconv-lite/encodings/tables/eucjp.json | 182 - .../encodings/tables/gb18030-ranges.json | 1 - .../encodings/tables/gbk-added.json | 55 - .../iconv-lite/encodings/tables/shiftjis.json | 125 - node_modules/iconv-lite/encodings/utf16.js | 177 - node_modules/iconv-lite/encodings/utf7.js | 290 - node_modules/iconv-lite/lib/bom-handling.js | 52 - node_modules/iconv-lite/lib/extend-node.js | 217 - node_modules/iconv-lite/lib/index.d.ts | 24 - node_modules/iconv-lite/lib/index.js | 153 - node_modules/iconv-lite/lib/streams.js | 121 - node_modules/iconv-lite/package.json | 46 - node_modules/ignore-by-default/LICENSE | 14 - node_modules/ignore-by-default/README.md | 26 - node_modules/ignore-by-default/index.js | 12 - node_modules/ignore-by-default/package.json | 34 - node_modules/inherits/LICENSE | 16 - node_modules/inherits/README.md | 42 - node_modules/inherits/inherits.js | 9 - node_modules/inherits/inherits_browser.js | 27 - node_modules/inherits/package.json | 29 - node_modules/ipaddr.js/LICENSE | 19 - node_modules/ipaddr.js/README.md | 233 - node_modules/ipaddr.js/ipaddr.min.js | 1 - node_modules/ipaddr.js/lib/ipaddr.js | 673 -- node_modules/ipaddr.js/lib/ipaddr.js.d.ts | 68 - node_modules/ipaddr.js/package.json | 35 - node_modules/is-binary-path/index.d.ts | 17 - node_modules/is-binary-path/index.js | 7 - node_modules/is-binary-path/license | 9 - node_modules/is-binary-path/package.json | 40 - node_modules/is-binary-path/readme.md | 34 - node_modules/is-extglob/LICENSE | 21 - node_modules/is-extglob/README.md | 107 - node_modules/is-extglob/index.js | 20 - node_modules/is-extglob/package.json | 69 - node_modules/is-glob/LICENSE | 21 - node_modules/is-glob/README.md | 206 - node_modules/is-glob/index.js | 150 - node_modules/is-glob/package.json | 81 - node_modules/is-number/LICENSE | 21 - node_modules/is-number/README.md | 187 - node_modules/is-number/index.js | 18 - node_modules/is-number/package.json | 82 - node_modules/math-intrinsics/.eslintrc | 16 - .../math-intrinsics/.github/FUNDING.yml | 12 - node_modules/math-intrinsics/CHANGELOG.md | 24 - node_modules/math-intrinsics/LICENSE | 21 - node_modules/math-intrinsics/README.md | 50 - node_modules/math-intrinsics/abs.d.ts | 1 - node_modules/math-intrinsics/abs.js | 4 - .../constants/maxArrayLength.d.ts | 3 - .../constants/maxArrayLength.js | 4 - .../constants/maxSafeInteger.d.ts | 3 - .../constants/maxSafeInteger.js | 5 - .../math-intrinsics/constants/maxValue.d.ts | 3 - .../math-intrinsics/constants/maxValue.js | 5 - node_modules/math-intrinsics/floor.d.ts | 1 - node_modules/math-intrinsics/floor.js | 4 - node_modules/math-intrinsics/isFinite.d.ts | 3 - node_modules/math-intrinsics/isFinite.js | 12 - node_modules/math-intrinsics/isInteger.d.ts | 3 - node_modules/math-intrinsics/isInteger.js | 16 - node_modules/math-intrinsics/isNaN.d.ts | 1 - node_modules/math-intrinsics/isNaN.js | 6 - .../math-intrinsics/isNegativeZero.d.ts | 3 - .../math-intrinsics/isNegativeZero.js | 6 - node_modules/math-intrinsics/max.d.ts | 1 - node_modules/math-intrinsics/max.js | 4 - node_modules/math-intrinsics/min.d.ts | 1 - node_modules/math-intrinsics/min.js | 4 - node_modules/math-intrinsics/mod.d.ts | 3 - node_modules/math-intrinsics/mod.js | 9 - node_modules/math-intrinsics/package.json | 86 - node_modules/math-intrinsics/pow.d.ts | 1 - node_modules/math-intrinsics/pow.js | 4 - node_modules/math-intrinsics/round.d.ts | 1 - node_modules/math-intrinsics/round.js | 4 - node_modules/math-intrinsics/sign.d.ts | 3 - node_modules/math-intrinsics/sign.js | 11 - node_modules/math-intrinsics/test/index.js | 192 - node_modules/math-intrinsics/tsconfig.json | 3 - node_modules/media-typer/HISTORY.md | 22 - node_modules/media-typer/LICENSE | 22 - node_modules/media-typer/README.md | 81 - node_modules/media-typer/index.js | 270 - node_modules/media-typer/package.json | 26 - node_modules/merge-descriptors/HISTORY.md | 21 - node_modules/merge-descriptors/LICENSE | 23 - node_modules/merge-descriptors/README.md | 49 - node_modules/merge-descriptors/index.js | 60 - node_modules/merge-descriptors/package.json | 39 - node_modules/methods/HISTORY.md | 29 - node_modules/methods/LICENSE | 24 - node_modules/methods/README.md | 51 - node_modules/methods/index.js | 69 - node_modules/methods/package.json | 36 - node_modules/mime-db/HISTORY.md | 507 - node_modules/mime-db/LICENSE | 23 - node_modules/mime-db/README.md | 100 - node_modules/mime-db/db.json | 8519 ----------------- node_modules/mime-db/index.js | 12 - node_modules/mime-db/package.json | 60 - node_modules/mime-types/HISTORY.md | 397 - node_modules/mime-types/LICENSE | 23 - node_modules/mime-types/README.md | 113 - node_modules/mime-types/index.js | 188 - node_modules/mime-types/package.json | 44 - node_modules/mime/.npmignore | 0 node_modules/mime/CHANGELOG.md | 164 - node_modules/mime/LICENSE | 21 - node_modules/mime/README.md | 90 - node_modules/mime/cli.js | 8 - node_modules/mime/mime.js | 108 - node_modules/mime/package.json | 44 - node_modules/mime/src/build.js | 53 - node_modules/mime/src/test.js | 60 - node_modules/mime/types.json | 1 - node_modules/minimatch/LICENSE | 15 - node_modules/minimatch/README.md | 230 - node_modules/minimatch/minimatch.js | 947 -- node_modules/minimatch/package.json | 33 - node_modules/ms/index.js | 152 - node_modules/ms/license.md | 21 - node_modules/ms/package.json | 37 - node_modules/ms/readme.md | 51 - node_modules/negotiator/HISTORY.md | 108 - node_modules/negotiator/LICENSE | 24 - node_modules/negotiator/README.md | 203 - node_modules/negotiator/index.js | 82 - node_modules/negotiator/lib/charset.js | 169 - node_modules/negotiator/lib/encoding.js | 184 - node_modules/negotiator/lib/language.js | 179 - node_modules/negotiator/lib/mediaType.js | 294 - node_modules/negotiator/package.json | 42 - node_modules/nodemon/.prettierrc.json | 3 - node_modules/nodemon/LICENSE | 21 - node_modules/nodemon/README.md | 441 - node_modules/nodemon/bin/nodemon.js | 16 - node_modules/nodemon/bin/windows-kill.exe | Bin 80384 -> 0 bytes node_modules/nodemon/doc/cli/authors.txt | 8 - node_modules/nodemon/doc/cli/config.txt | 44 - node_modules/nodemon/doc/cli/help.txt | 29 - node_modules/nodemon/doc/cli/logo.txt | 20 - node_modules/nodemon/doc/cli/options.txt | 36 - node_modules/nodemon/doc/cli/topics.txt | 8 - node_modules/nodemon/doc/cli/usage.txt | 3 - node_modules/nodemon/doc/cli/whoami.txt | 9 - node_modules/nodemon/index.d.ts | 125 - node_modules/nodemon/jsconfig.json | 7 - node_modules/nodemon/lib/cli/index.js | 49 - node_modules/nodemon/lib/cli/parse.js | 230 - node_modules/nodemon/lib/config/command.js | 43 - node_modules/nodemon/lib/config/defaults.js | 34 - node_modules/nodemon/lib/config/exec.js | 234 - node_modules/nodemon/lib/config/index.js | 93 - node_modules/nodemon/lib/config/load.js | 225 - node_modules/nodemon/lib/help/index.js | 27 - node_modules/nodemon/lib/index.js | 1 - node_modules/nodemon/lib/monitor/index.js | 4 - node_modules/nodemon/lib/monitor/match.js | 287 - node_modules/nodemon/lib/monitor/run.js | 562 -- node_modules/nodemon/lib/monitor/signals.js | 34 - node_modules/nodemon/lib/monitor/watch.js | 244 - node_modules/nodemon/lib/nodemon.js | 317 - node_modules/nodemon/lib/rules/add.js | 89 - node_modules/nodemon/lib/rules/index.js | 53 - node_modules/nodemon/lib/rules/parse.js | 43 - node_modules/nodemon/lib/spawn.js | 74 - node_modules/nodemon/lib/utils/bus.js | 44 - node_modules/nodemon/lib/utils/clone.js | 40 - node_modules/nodemon/lib/utils/colour.js | 26 - node_modules/nodemon/lib/utils/index.js | 103 - node_modules/nodemon/lib/utils/log.js | 82 - node_modules/nodemon/lib/utils/merge.js | 47 - node_modules/nodemon/lib/version.js | 100 - .../nodemon/node_modules/debug/LICENSE | 20 - .../nodemon/node_modules/debug/README.md | 481 - .../nodemon/node_modules/debug/package.json | 64 - .../nodemon/node_modules/debug/src/browser.js | 272 - .../nodemon/node_modules/debug/src/common.js | 292 - .../nodemon/node_modules/debug/src/index.js | 10 - .../nodemon/node_modules/debug/src/node.js | 263 - node_modules/nodemon/node_modules/ms/index.js | 162 - .../nodemon/node_modules/ms/license.md | 21 - .../nodemon/node_modules/ms/package.json | 38 - .../nodemon/node_modules/ms/readme.md | 59 - node_modules/nodemon/package.json | 75 - node_modules/normalize-path/LICENSE | 21 - node_modules/normalize-path/README.md | 127 - node_modules/normalize-path/index.js | 35 - node_modules/normalize-path/package.json | 77 - node_modules/object-assign/index.js | 90 - node_modules/object-assign/license | 21 - node_modules/object-assign/package.json | 42 - node_modules/object-assign/readme.md | 61 - node_modules/object-inspect/.eslintrc | 53 - .../object-inspect/.github/FUNDING.yml | 12 - node_modules/object-inspect/.nycrc | 13 - node_modules/object-inspect/CHANGELOG.md | 424 - node_modules/object-inspect/LICENSE | 21 - node_modules/object-inspect/example/all.js | 23 - .../object-inspect/example/circular.js | 6 - node_modules/object-inspect/example/fn.js | 5 - .../object-inspect/example/inspect.js | 10 - node_modules/object-inspect/index.js | 544 -- .../object-inspect/package-support.json | 20 - node_modules/object-inspect/package.json | 105 - node_modules/object-inspect/readme.markdown | 84 - node_modules/object-inspect/test-core-js.js | 26 - node_modules/object-inspect/test/bigint.js | 58 - .../object-inspect/test/browser/dom.js | 15 - node_modules/object-inspect/test/circular.js | 16 - node_modules/object-inspect/test/deep.js | 12 - node_modules/object-inspect/test/element.js | 53 - node_modules/object-inspect/test/err.js | 48 - node_modules/object-inspect/test/fakes.js | 29 - node_modules/object-inspect/test/fn.js | 76 - node_modules/object-inspect/test/global.js | 17 - node_modules/object-inspect/test/has.js | 15 - node_modules/object-inspect/test/holes.js | 15 - .../object-inspect/test/indent-option.js | 271 - node_modules/object-inspect/test/inspect.js | 139 - node_modules/object-inspect/test/lowbyte.js | 12 - node_modules/object-inspect/test/number.js | 58 - .../object-inspect/test/quoteStyle.js | 26 - .../object-inspect/test/toStringTag.js | 40 - node_modules/object-inspect/test/undef.js | 12 - node_modules/object-inspect/test/values.js | 261 - node_modules/object-inspect/util.inspect.js | 1 - node_modules/on-finished/HISTORY.md | 98 - node_modules/on-finished/LICENSE | 23 - node_modules/on-finished/README.md | 162 - node_modules/on-finished/index.js | 234 - node_modules/on-finished/package.json | 39 - node_modules/parseurl/HISTORY.md | 58 - node_modules/parseurl/LICENSE | 24 - node_modules/parseurl/README.md | 133 - node_modules/parseurl/index.js | 158 - node_modules/parseurl/package.json | 40 - node_modules/path-to-regexp/LICENSE | 21 - node_modules/path-to-regexp/Readme.md | 35 - node_modules/path-to-regexp/index.js | 156 - node_modules/path-to-regexp/package.json | 30 - node_modules/picomatch/CHANGELOG.md | 136 - node_modules/picomatch/LICENSE | 21 - node_modules/picomatch/README.md | 708 -- node_modules/picomatch/index.js | 3 - node_modules/picomatch/lib/constants.js | 179 - node_modules/picomatch/lib/parse.js | 1091 --- node_modules/picomatch/lib/picomatch.js | 342 - node_modules/picomatch/lib/scan.js | 391 - node_modules/picomatch/lib/utils.js | 64 - node_modules/picomatch/package.json | 81 - node_modules/proxy-addr/HISTORY.md | 161 - node_modules/proxy-addr/LICENSE | 22 - node_modules/proxy-addr/README.md | 139 - node_modules/proxy-addr/index.js | 327 - node_modules/proxy-addr/package.json | 47 - node_modules/pstree.remy/.travis.yml | 8 - node_modules/pstree.remy/LICENSE | 7 - node_modules/pstree.remy/README.md | 26 - node_modules/pstree.remy/lib/index.js | 37 - node_modules/pstree.remy/lib/tree.js | 37 - node_modules/pstree.remy/lib/utils.js | 53 - node_modules/pstree.remy/package.json | 33 - .../pstree.remy/tests/fixtures/index.js | 13 - node_modules/pstree.remy/tests/fixtures/out1 | 10 - node_modules/pstree.remy/tests/fixtures/out2 | 29 - node_modules/pstree.remy/tests/index.test.js | 51 - node_modules/qs/.editorconfig | 46 - node_modules/qs/.eslintrc | 38 - node_modules/qs/.github/FUNDING.yml | 12 - node_modules/qs/.nycrc | 13 - node_modules/qs/CHANGELOG.md | 600 -- node_modules/qs/LICENSE.md | 29 - node_modules/qs/README.md | 709 -- node_modules/qs/dist/qs.js | 90 - node_modules/qs/lib/formats.js | 23 - node_modules/qs/lib/index.js | 11 - node_modules/qs/lib/parse.js | 296 - node_modules/qs/lib/stringify.js | 351 - node_modules/qs/lib/utils.js | 265 - node_modules/qs/package.json | 91 - node_modules/qs/test/empty-keys-cases.js | 267 - node_modules/qs/test/parse.js | 1170 --- node_modules/qs/test/stringify.js | 1298 --- node_modules/qs/test/utils.js | 136 - node_modules/range-parser/HISTORY.md | 56 - node_modules/range-parser/LICENSE | 23 - node_modules/range-parser/README.md | 84 - node_modules/range-parser/index.js | 162 - node_modules/range-parser/package.json | 44 - node_modules/raw-body/HISTORY.md | 308 - node_modules/raw-body/LICENSE | 22 - node_modules/raw-body/README.md | 223 - node_modules/raw-body/SECURITY.md | 24 - node_modules/raw-body/index.d.ts | 87 - node_modules/raw-body/index.js | 336 - node_modules/raw-body/package.json | 49 - node_modules/readdirp/LICENSE | 21 - node_modules/readdirp/README.md | 122 - node_modules/readdirp/index.d.ts | 43 - node_modules/readdirp/index.js | 287 - node_modules/readdirp/package.json | 122 - node_modules/safe-buffer/LICENSE | 21 - node_modules/safe-buffer/README.md | 584 -- node_modules/safe-buffer/index.d.ts | 187 - node_modules/safe-buffer/index.js | 65 - node_modules/safe-buffer/package.json | 51 - node_modules/safer-buffer/LICENSE | 21 - node_modules/safer-buffer/Porting-Buffer.md | 268 - node_modules/safer-buffer/Readme.md | 156 - node_modules/safer-buffer/dangerous.js | 58 - node_modules/safer-buffer/package.json | 34 - node_modules/safer-buffer/safer.js | 77 - node_modules/safer-buffer/tests.js | 406 - node_modules/semver/LICENSE | 15 - node_modules/semver/README.md | 664 -- node_modules/semver/bin/semver.js | 191 - node_modules/semver/classes/comparator.js | 143 - node_modules/semver/classes/index.js | 7 - node_modules/semver/classes/range.js | 556 -- node_modules/semver/classes/semver.js | 319 - node_modules/semver/functions/clean.js | 8 - node_modules/semver/functions/cmp.js | 54 - node_modules/semver/functions/coerce.js | 62 - .../semver/functions/compare-build.js | 9 - .../semver/functions/compare-loose.js | 5 - node_modules/semver/functions/compare.js | 7 - node_modules/semver/functions/diff.js | 60 - node_modules/semver/functions/eq.js | 5 - node_modules/semver/functions/gt.js | 5 - node_modules/semver/functions/gte.js | 5 - node_modules/semver/functions/inc.js | 21 - node_modules/semver/functions/lt.js | 5 - node_modules/semver/functions/lte.js | 5 - node_modules/semver/functions/major.js | 5 - node_modules/semver/functions/minor.js | 5 - node_modules/semver/functions/neq.js | 5 - node_modules/semver/functions/parse.js | 18 - node_modules/semver/functions/patch.js | 5 - node_modules/semver/functions/prerelease.js | 8 - node_modules/semver/functions/rcompare.js | 5 - node_modules/semver/functions/rsort.js | 5 - node_modules/semver/functions/satisfies.js | 12 - node_modules/semver/functions/sort.js | 5 - node_modules/semver/functions/valid.js | 8 - node_modules/semver/index.js | 91 - node_modules/semver/internal/constants.js | 37 - node_modules/semver/internal/debug.js | 11 - node_modules/semver/internal/identifiers.js | 25 - node_modules/semver/internal/lrucache.js | 42 - node_modules/semver/internal/parse-options.js | 17 - node_modules/semver/internal/re.js | 223 - node_modules/semver/package.json | 78 - node_modules/semver/preload.js | 4 - node_modules/semver/range.bnf | 16 - node_modules/semver/ranges/gtr.js | 6 - node_modules/semver/ranges/intersects.js | 9 - node_modules/semver/ranges/ltr.js | 6 - node_modules/semver/ranges/max-satisfying.js | 27 - node_modules/semver/ranges/min-satisfying.js | 26 - node_modules/semver/ranges/min-version.js | 63 - node_modules/semver/ranges/outside.js | 82 - node_modules/semver/ranges/simplify.js | 49 - node_modules/semver/ranges/subset.js | 249 - node_modules/semver/ranges/to-comparators.js | 10 - node_modules/semver/ranges/valid.js | 13 - node_modules/send/HISTORY.md | 526 - node_modules/send/LICENSE | 23 - node_modules/send/README.md | 327 - node_modules/send/SECURITY.md | 24 - node_modules/send/index.js | 1142 --- .../send/node_modules/encodeurl/HISTORY.md | 14 - .../send/node_modules/encodeurl/LICENSE | 22 - .../send/node_modules/encodeurl/README.md | 128 - .../send/node_modules/encodeurl/index.js | 60 - .../send/node_modules/encodeurl/package.json | 40 - node_modules/send/node_modules/ms/index.js | 162 - node_modules/send/node_modules/ms/license.md | 21 - .../send/node_modules/ms/package.json | 38 - node_modules/send/node_modules/ms/readme.md | 59 - node_modules/send/package.json | 62 - node_modules/serve-static/HISTORY.md | 487 - node_modules/serve-static/LICENSE | 25 - node_modules/serve-static/README.md | 257 - node_modules/serve-static/index.js | 209 - node_modules/serve-static/package.json | 42 - node_modules/setprototypeof/LICENSE | 13 - node_modules/setprototypeof/README.md | 31 - node_modules/setprototypeof/index.d.ts | 2 - node_modules/setprototypeof/index.js | 17 - node_modules/setprototypeof/package.json | 38 - node_modules/setprototypeof/test/index.js | 24 - node_modules/side-channel-list/.editorconfig | 9 - node_modules/side-channel-list/.eslintrc | 11 - .../side-channel-list/.github/FUNDING.yml | 12 - node_modules/side-channel-list/.nycrc | 13 - node_modules/side-channel-list/CHANGELOG.md | 15 - node_modules/side-channel-list/LICENSE | 21 - node_modules/side-channel-list/README.md | 62 - node_modules/side-channel-list/index.d.ts | 13 - node_modules/side-channel-list/index.js | 113 - node_modules/side-channel-list/list.d.ts | 14 - node_modules/side-channel-list/package.json | 77 - node_modules/side-channel-list/test/index.js | 104 - node_modules/side-channel-list/tsconfig.json | 9 - node_modules/side-channel-map/.editorconfig | 9 - node_modules/side-channel-map/.eslintrc | 11 - .../side-channel-map/.github/FUNDING.yml | 12 - node_modules/side-channel-map/.nycrc | 13 - node_modules/side-channel-map/CHANGELOG.md | 22 - node_modules/side-channel-map/LICENSE | 21 - node_modules/side-channel-map/README.md | 62 - node_modules/side-channel-map/index.d.ts | 15 - node_modules/side-channel-map/index.js | 68 - node_modules/side-channel-map/package.json | 80 - node_modules/side-channel-map/test/index.js | 114 - node_modules/side-channel-map/tsconfig.json | 9 - .../side-channel-weakmap/.editorconfig | 9 - node_modules/side-channel-weakmap/.eslintrc | 12 - .../side-channel-weakmap/.github/FUNDING.yml | 12 - node_modules/side-channel-weakmap/.nycrc | 13 - .../side-channel-weakmap/CHANGELOG.md | 28 - node_modules/side-channel-weakmap/LICENSE | 21 - node_modules/side-channel-weakmap/README.md | 62 - node_modules/side-channel-weakmap/index.d.ts | 15 - node_modules/side-channel-weakmap/index.js | 84 - .../side-channel-weakmap/package.json | 87 - .../side-channel-weakmap/test/index.js | 114 - .../side-channel-weakmap/tsconfig.json | 9 - node_modules/side-channel/.editorconfig | 9 - node_modules/side-channel/.eslintrc | 12 - node_modules/side-channel/.github/FUNDING.yml | 12 - node_modules/side-channel/.nycrc | 13 - node_modules/side-channel/CHANGELOG.md | 110 - node_modules/side-channel/LICENSE | 21 - node_modules/side-channel/README.md | 61 - node_modules/side-channel/index.d.ts | 14 - node_modules/side-channel/index.js | 43 - node_modules/side-channel/package.json | 85 - node_modules/side-channel/test/index.js | 104 - node_modules/side-channel/tsconfig.json | 9 - node_modules/simple-update-notifier/LICENSE | 21 - node_modules/simple-update-notifier/README.md | 82 - .../simple-update-notifier/build/index.d.ts | 13 - .../simple-update-notifier/build/index.js | 210 - .../simple-update-notifier/package.json | 100 - .../src/borderedText.ts | 12 - .../simple-update-notifier/src/cache.spec.ts | 17 - .../simple-update-notifier/src/cache.ts | 44 - .../src/getDistVersion.spec.ts | 35 - .../src/getDistVersion.ts | 29 - .../src/hasNewVersion.spec.ts | 82 - .../src/hasNewVersion.ts | 40 - .../simple-update-notifier/src/index.spec.ts | 27 - .../simple-update-notifier/src/index.ts | 34 - .../simple-update-notifier/src/isNpmOrYarn.ts | 12 - .../simple-update-notifier/src/types.ts | 8 - node_modules/socket.io-adapter/LICENSE | 20 - node_modules/socket.io-adapter/Readme.md | 23 - .../dist/cluster-adapter.d.ts | 201 - .../socket.io-adapter/dist/cluster-adapter.js | 674 -- .../socket.io-adapter/dist/contrib/yeast.d.ts | 23 - .../socket.io-adapter/dist/contrib/yeast.js | 55 - .../dist/in-memory-adapter.d.ts | 179 - .../dist/in-memory-adapter.js | 394 - .../socket.io-adapter/dist/index.d.ts | 2 - node_modules/socket.io-adapter/dist/index.js | 10 - .../node_modules/debug/LICENSE | 20 - .../node_modules/debug/README.md | 481 - .../node_modules/debug/package.json | 60 - .../node_modules/debug/src/browser.js | 271 - .../node_modules/debug/src/common.js | 274 - .../node_modules/debug/src/index.js | 10 - .../node_modules/debug/src/node.js | 263 - .../node_modules/ms/index.js | 162 - .../node_modules/ms/license.md | 21 - .../node_modules/ms/package.json | 38 - .../node_modules/ms/readme.md | 59 - node_modules/socket.io-adapter/package.json | 39 - node_modules/socket.io-parser/LICENSE | 20 - node_modules/socket.io-parser/Readme.md | 81 - .../socket.io-parser/build/cjs/binary.d.ts | 20 - .../socket.io-parser/build/cjs/binary.js | 88 - .../socket.io-parser/build/cjs/index.d.ts | 90 - .../socket.io-parser/build/cjs/index.js | 321 - .../socket.io-parser/build/cjs/is-binary.d.ts | 7 - .../socket.io-parser/build/cjs/is-binary.js | 55 - .../socket.io-parser/build/cjs/package.json | 3 - .../build/esm-debug/binary.d.ts | 20 - .../build/esm-debug/binary.js | 83 - .../build/esm-debug/index.d.ts | 90 - .../socket.io-parser/build/esm-debug/index.js | 316 - .../build/esm-debug/is-binary.d.ts | 7 - .../build/esm-debug/is-binary.js | 50 - .../build/esm-debug/package.json | 3 - .../socket.io-parser/build/esm/binary.d.ts | 20 - .../socket.io-parser/build/esm/binary.js | 83 - .../socket.io-parser/build/esm/index.d.ts | 90 - .../socket.io-parser/build/esm/index.js | 311 - .../socket.io-parser/build/esm/is-binary.d.ts | 7 - .../socket.io-parser/build/esm/is-binary.js | 50 - .../socket.io-parser/build/esm/package.json | 3 - .../node_modules/debug/LICENSE | 20 - .../node_modules/debug/README.md | 481 - .../node_modules/debug/package.json | 60 - .../node_modules/debug/src/browser.js | 271 - .../node_modules/debug/src/common.js | 274 - .../node_modules/debug/src/index.js | 10 - .../node_modules/debug/src/node.js | 263 - .../socket.io-parser/node_modules/ms/index.js | 162 - .../node_modules/ms/license.md | 21 - .../node_modules/ms/package.json | 38 - .../node_modules/ms/readme.md | 59 - node_modules/socket.io-parser/package.json | 58 - node_modules/socket.io/LICENSE | 22 - node_modules/socket.io/Readme.md | 273 - .../client-dist/socket.io.esm.min.js | 7 - .../client-dist/socket.io.esm.min.js.map | 1 - .../socket.io/client-dist/socket.io.js | 4908 ---------- .../socket.io/client-dist/socket.io.js.map | 1 - .../socket.io/client-dist/socket.io.min.js | 7 - .../client-dist/socket.io.min.js.map | 1 - .../client-dist/socket.io.msgpack.min.js | 7 - .../client-dist/socket.io.msgpack.min.js.map | 1 - .../socket.io/dist/broadcast-operator.d.ts | 283 - .../socket.io/dist/broadcast-operator.js | 436 - node_modules/socket.io/dist/client.d.ts | 119 - node_modules/socket.io/dist/client.js | 268 - node_modules/socket.io/dist/index.d.ts | 593 -- node_modules/socket.io/dist/index.js | 804 -- node_modules/socket.io/dist/namespace.d.ts | 432 - node_modules/socket.io/dist/namespace.js | 581 -- .../socket.io/dist/parent-namespace.d.ts | 30 - .../socket.io/dist/parent-namespace.js | 88 - node_modules/socket.io/dist/socket-types.d.ts | 56 - node_modules/socket.io/dist/socket-types.js | 11 - node_modules/socket.io/dist/socket.d.ts | 613 -- node_modules/socket.io/dist/socket.js | 977 -- node_modules/socket.io/dist/typed-events.d.ts | 203 - node_modules/socket.io/dist/typed-events.js | 81 - node_modules/socket.io/dist/uws.d.ts | 3 - node_modules/socket.io/dist/uws.js | 136 - .../socket.io/node_modules/debug/LICENSE | 20 - .../socket.io/node_modules/debug/README.md | 481 - .../socket.io/node_modules/debug/package.json | 60 - .../node_modules/debug/src/browser.js | 271 - .../node_modules/debug/src/common.js | 274 - .../socket.io/node_modules/debug/src/index.js | 10 - .../socket.io/node_modules/debug/src/node.js | 263 - .../socket.io/node_modules/ms/index.js | 162 - .../socket.io/node_modules/ms/license.md | 21 - .../socket.io/node_modules/ms/package.json | 38 - .../socket.io/node_modules/ms/readme.md | 59 - node_modules/socket.io/package.json | 85 - node_modules/socket.io/wrapper.mjs | 3 - node_modules/statuses/HISTORY.md | 82 - node_modules/statuses/LICENSE | 23 - node_modules/statuses/README.md | 136 - node_modules/statuses/codes.json | 65 - node_modules/statuses/index.js | 146 - node_modules/statuses/package.json | 49 - node_modules/supports-color/browser.js | 5 - node_modules/supports-color/index.js | 131 - node_modules/supports-color/license | 9 - node_modules/supports-color/package.json | 53 - node_modules/supports-color/readme.md | 66 - node_modules/to-regex-range/LICENSE | 21 - node_modules/to-regex-range/README.md | 305 - node_modules/to-regex-range/index.js | 288 - node_modules/to-regex-range/package.json | 88 - node_modules/toidentifier/HISTORY.md | 9 - node_modules/toidentifier/LICENSE | 21 - node_modules/toidentifier/README.md | 61 - node_modules/toidentifier/index.js | 32 - node_modules/toidentifier/package.json | 38 - node_modules/touch/LICENSE | 15 - node_modules/touch/README.md | 52 - node_modules/touch/bin/nodetouch.js | 112 - node_modules/touch/index.js | 224 - node_modules/touch/package.json | 25 - node_modules/type-is/HISTORY.md | 259 - node_modules/type-is/LICENSE | 23 - node_modules/type-is/README.md | 170 - node_modules/type-is/index.js | 266 - node_modules/type-is/package.json | 45 - .../undefsafe/.github/workflows/release.yml | 25 - node_modules/undefsafe/.jscsrc | 13 - node_modules/undefsafe/.jshintrc | 16 - node_modules/undefsafe/.travis.yml | 18 - node_modules/undefsafe/LICENSE | 22 - node_modules/undefsafe/README.md | 63 - node_modules/undefsafe/example.js | 14 - node_modules/undefsafe/lib/undefsafe.js | 125 - node_modules/undefsafe/package.json | 34 - node_modules/undici-types/LICENSE | 21 - node_modules/undici-types/README.md | 6 - node_modules/undici-types/agent.d.ts | 35 - node_modules/undici-types/api.d.ts | 43 - node_modules/undici-types/balanced-pool.d.ts | 29 - .../undici-types/cache-interceptor.d.ts | 172 - node_modules/undici-types/cache.d.ts | 36 - node_modules/undici-types/client-stats.d.ts | 15 - node_modules/undici-types/client.d.ts | 110 - node_modules/undici-types/connector.d.ts | 34 - node_modules/undici-types/content-type.d.ts | 21 - node_modules/undici-types/cookies.d.ts | 30 - .../undici-types/diagnostics-channel.d.ts | 75 - node_modules/undici-types/dispatcher.d.ts | 282 - .../undici-types/env-http-proxy-agent.d.ts | 22 - node_modules/undici-types/errors.d.ts | 171 - node_modules/undici-types/eventsource.d.ts | 61 - node_modules/undici-types/fetch.d.ts | 211 - node_modules/undici-types/formdata.d.ts | 108 - .../undici-types/global-dispatcher.d.ts | 9 - node_modules/undici-types/global-origin.d.ts | 7 - node_modules/undici-types/h2c-client.d.ts | 75 - node_modules/undici-types/handlers.d.ts | 15 - node_modules/undici-types/header.d.ts | 160 - node_modules/undici-types/index.d.ts | 75 - node_modules/undici-types/interceptors.d.ts | 34 - node_modules/undici-types/mock-agent.d.ts | 68 - .../undici-types/mock-call-history.d.ts | 111 - node_modules/undici-types/mock-client.d.ts | 27 - node_modules/undici-types/mock-errors.d.ts | 12 - .../undici-types/mock-interceptor.d.ts | 95 - node_modules/undici-types/mock-pool.d.ts | 27 - node_modules/undici-types/package.json | 55 - node_modules/undici-types/patch.d.ts | 29 - node_modules/undici-types/pool-stats.d.ts | 19 - node_modules/undici-types/pool.d.ts | 41 - node_modules/undici-types/proxy-agent.d.ts | 29 - node_modules/undici-types/readable.d.ts | 68 - node_modules/undici-types/retry-agent.d.ts | 8 - node_modules/undici-types/retry-handler.d.ts | 125 - node_modules/undici-types/util.d.ts | 18 - node_modules/undici-types/utility.d.ts | 7 - node_modules/undici-types/webidl.d.ts | 280 - node_modules/undici-types/websocket.d.ts | 186 - node_modules/unpipe/HISTORY.md | 4 - node_modules/unpipe/LICENSE | 22 - node_modules/unpipe/README.md | 43 - node_modules/unpipe/index.js | 69 - node_modules/unpipe/package.json | 27 - node_modules/utils-merge/.npmignore | 9 - node_modules/utils-merge/LICENSE | 20 - node_modules/utils-merge/README.md | 34 - node_modules/utils-merge/index.js | 23 - node_modules/utils-merge/package.json | 40 - node_modules/vary/HISTORY.md | 39 - node_modules/vary/LICENSE | 22 - node_modules/vary/README.md | 101 - node_modules/vary/index.js | 149 - node_modules/vary/package.json | 43 - node_modules/ws/LICENSE | 20 - node_modules/ws/README.md | 548 -- node_modules/ws/browser.js | 8 - node_modules/ws/index.js | 13 - node_modules/ws/lib/buffer-util.js | 131 - node_modules/ws/lib/constants.js | 12 - node_modules/ws/lib/event-target.js | 292 - node_modules/ws/lib/extension.js | 203 - node_modules/ws/lib/limiter.js | 55 - node_modules/ws/lib/permessage-deflate.js | 514 - node_modules/ws/lib/receiver.js | 704 -- node_modules/ws/lib/sender.js | 497 - node_modules/ws/lib/stream.js | 159 - node_modules/ws/lib/subprotocol.js | 62 - node_modules/ws/lib/validation.js | 130 - node_modules/ws/lib/websocket-server.js | 540 -- node_modules/ws/lib/websocket.js | 1338 --- node_modules/ws/package.json | 69 - node_modules/ws/wrapper.mjs | 8 - package-lock.json | 1341 --- package.json | 18 - server_test.py | 123 + test-server.js | 64 - 1306 files changed, 8820 insertions(+), 188539 deletions(-) create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/appInsightsSettings.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/copilot.data.migration.agent.xml create mode 100644 .idea/copilot.data.migration.ask.xml create mode 100644 .idea/copilot.data.migration.ask2agent.xml create mode 100644 .idea/copilot.data.migration.edit.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 .kotlin/errors/errors-1759321347725.log create mode 100644 .kotlin/errors/errors-1759400006923.log create mode 100644 .kotlin/errors/errors-1759466115565.log create mode 100644 .kotlin/errors/errors-1759530749084.log create mode 100644 app/build-legacy.gradle.kts create mode 100644 app/src/main/AndroidManifest-legacy.xml create mode 100644 app/src/main/java/com/example/godeye/GodEyeApplication.kt create mode 100644 app/src/main/java/com/example/godeye/LegacyCameraActivity.kt create mode 100644 app/src/main/java/com/example/godeye/LegacyMainActivity.kt create mode 100644 app/src/main/java/com/example/godeye/MainViewModel.kt create mode 100644 app/src/main/java/com/example/godeye/SettingsScreen.kt create mode 100644 app/src/main/java/com/example/godeye/camera/CameraManager.kt create mode 100644 app/src/main/java/com/example/godeye/camera/CameraScreen.kt create mode 100644 app/src/main/java/com/example/godeye/managers/Camera2Manager.kt delete mode 100644 app/src/main/java/com/example/godeye/managers/CameraManager.kt delete mode 100644 app/src/main/java/com/example/godeye/managers/WebRTCManager.kt create mode 100644 app/src/main/java/com/example/godeye/models/CameraModels.kt create mode 100644 app/src/main/java/com/example/godeye/models/SocketEvents.kt delete mode 100644 app/src/main/java/com/example/godeye/services/CameraService.kt create mode 100644 app/src/main/java/com/example/godeye/streaming/HLSStreamManager.kt create mode 100644 app/src/main/java/com/example/godeye/streaming/RTSPStreamManager.kt create mode 100644 app/src/main/java/com/example/godeye/streaming/UDPStreamManager.kt create mode 100644 app/src/main/java/com/example/godeye/streaming/UnifiedStreamingManager.kt create mode 100644 app/src/main/java/com/example/godeye/streaming/WebRTCStreamManager.kt create mode 100644 app/src/main/java/com/example/godeye/ui/components/AnimatedComponents.kt delete mode 100644 app/src/main/java/com/example/godeye/ui/components/CameraRequestDialog.kt delete mode 100644 app/src/main/java/com/example/godeye/ui/components/ConnectionStatusCard.kt create mode 100644 app/src/main/java/com/example/godeye/ui/components/MainScreenComponents.kt delete mode 100644 app/src/main/java/com/example/godeye/ui/components/SessionsList.kt create mode 100644 app/src/main/java/com/example/godeye/ui/components/SettingsScreen.kt create mode 100644 app/src/main/java/com/example/godeye/ui/dialogs/CameraRequestDialog.kt delete mode 100644 app/src/main/java/com/example/godeye/ui/screens/MainScreen.kt delete mode 100644 app/src/main/java/com/example/godeye/ui/theme/Color.kt create mode 100644 app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt delete mode 100644 app/src/main/java/com/example/godeye/ui/theme/Theme.kt delete mode 100644 app/src/main/java/com/example/godeye/ui/theme/Type.kt delete mode 100644 app/src/main/java/com/example/godeye/ui/viewmodels/MainViewModel.kt create mode 100644 app/src/main/java/com/example/godeye/utils/ErrorHandler.kt create mode 100644 app/src/main/java/com/example/godeye/utils/Logger.kt create mode 100644 app/src/main/java/com/example/godeye/webrtc/WebRTCManager.kt create mode 100644 app/src/main/res/drawable/circle_button_background.xml create mode 100644 app/src/main/res/layout/activity_legacy_camera.xml create mode 100644 app/src/main/res/layout/activity_legacy_main.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100755 install_to_lg_g6.sh delete mode 120000 node_modules/.bin/mime delete mode 120000 node_modules/.bin/nodemon delete mode 120000 node_modules/.bin/nodetouch delete mode 120000 node_modules/.bin/semver delete mode 100644 node_modules/.package-lock.json delete mode 100644 node_modules/@socket.io/component-emitter/LICENSE delete mode 100644 node_modules/@socket.io/component-emitter/Readme.md delete mode 100644 node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts delete mode 100644 node_modules/@socket.io/component-emitter/lib/cjs/index.js delete mode 100644 node_modules/@socket.io/component-emitter/lib/cjs/package.json delete mode 100644 node_modules/@socket.io/component-emitter/lib/esm/index.d.ts delete mode 100644 node_modules/@socket.io/component-emitter/lib/esm/index.js delete mode 100644 node_modules/@socket.io/component-emitter/lib/esm/package.json delete mode 100644 node_modules/@socket.io/component-emitter/package.json delete mode 100644 node_modules/@types/cors/LICENSE delete mode 100644 node_modules/@types/cors/README.md delete mode 100644 node_modules/@types/cors/index.d.ts delete mode 100644 node_modules/@types/cors/package.json delete mode 100644 node_modules/@types/node/LICENSE delete mode 100644 node_modules/@types/node/README.md delete mode 100644 node_modules/@types/node/assert.d.ts delete mode 100644 node_modules/@types/node/assert/strict.d.ts delete mode 100644 node_modules/@types/node/async_hooks.d.ts delete mode 100644 node_modules/@types/node/buffer.buffer.d.ts delete mode 100644 node_modules/@types/node/buffer.d.ts delete mode 100644 node_modules/@types/node/child_process.d.ts delete mode 100644 node_modules/@types/node/cluster.d.ts delete mode 100644 node_modules/@types/node/compatibility/iterators.d.ts delete mode 100644 node_modules/@types/node/console.d.ts delete mode 100644 node_modules/@types/node/constants.d.ts delete mode 100644 node_modules/@types/node/crypto.d.ts delete mode 100644 node_modules/@types/node/dgram.d.ts delete mode 100644 node_modules/@types/node/diagnostics_channel.d.ts delete mode 100644 node_modules/@types/node/dns.d.ts delete mode 100644 node_modules/@types/node/dns/promises.d.ts delete mode 100644 node_modules/@types/node/domain.d.ts delete mode 100644 node_modules/@types/node/events.d.ts delete mode 100644 node_modules/@types/node/fs.d.ts delete mode 100644 node_modules/@types/node/fs/promises.d.ts delete mode 100644 node_modules/@types/node/globals.d.ts delete mode 100644 node_modules/@types/node/globals.typedarray.d.ts delete mode 100644 node_modules/@types/node/http.d.ts delete mode 100644 node_modules/@types/node/http2.d.ts delete mode 100644 node_modules/@types/node/https.d.ts delete mode 100644 node_modules/@types/node/index.d.ts delete mode 100644 node_modules/@types/node/inspector.d.ts delete mode 100644 node_modules/@types/node/inspector.generated.d.ts delete mode 100644 node_modules/@types/node/module.d.ts delete mode 100644 node_modules/@types/node/net.d.ts delete mode 100644 node_modules/@types/node/os.d.ts delete mode 100644 node_modules/@types/node/package.json delete mode 100644 node_modules/@types/node/path.d.ts delete mode 100644 node_modules/@types/node/perf_hooks.d.ts delete mode 100644 node_modules/@types/node/process.d.ts delete mode 100644 node_modules/@types/node/punycode.d.ts delete mode 100644 node_modules/@types/node/querystring.d.ts delete mode 100644 node_modules/@types/node/readline.d.ts delete mode 100644 node_modules/@types/node/readline/promises.d.ts delete mode 100644 node_modules/@types/node/repl.d.ts delete mode 100644 node_modules/@types/node/sea.d.ts delete mode 100644 node_modules/@types/node/sqlite.d.ts delete mode 100644 node_modules/@types/node/stream.d.ts delete mode 100644 node_modules/@types/node/stream/consumers.d.ts delete mode 100644 node_modules/@types/node/stream/promises.d.ts delete mode 100644 node_modules/@types/node/stream/web.d.ts delete mode 100644 node_modules/@types/node/string_decoder.d.ts delete mode 100644 node_modules/@types/node/test.d.ts delete mode 100644 node_modules/@types/node/timers.d.ts delete mode 100644 node_modules/@types/node/timers/promises.d.ts delete mode 100644 node_modules/@types/node/tls.d.ts delete mode 100644 node_modules/@types/node/trace_events.d.ts delete mode 100644 node_modules/@types/node/ts5.6/buffer.buffer.d.ts delete mode 100644 node_modules/@types/node/ts5.6/compatibility/float16array.d.ts delete mode 100644 node_modules/@types/node/ts5.6/globals.typedarray.d.ts delete mode 100644 node_modules/@types/node/ts5.6/index.d.ts delete mode 100644 node_modules/@types/node/ts5.7/compatibility/float16array.d.ts delete mode 100644 node_modules/@types/node/ts5.7/index.d.ts delete mode 100644 node_modules/@types/node/tty.d.ts delete mode 100644 node_modules/@types/node/url.d.ts delete mode 100644 node_modules/@types/node/util.d.ts delete mode 100644 node_modules/@types/node/v8.d.ts delete mode 100644 node_modules/@types/node/vm.d.ts delete mode 100644 node_modules/@types/node/wasi.d.ts delete mode 100644 node_modules/@types/node/web-globals/abortcontroller.d.ts delete mode 100644 node_modules/@types/node/web-globals/domexception.d.ts delete mode 100644 node_modules/@types/node/web-globals/events.d.ts delete mode 100644 node_modules/@types/node/web-globals/fetch.d.ts delete mode 100644 node_modules/@types/node/web-globals/navigator.d.ts delete mode 100644 node_modules/@types/node/web-globals/storage.d.ts delete mode 100644 node_modules/@types/node/worker_threads.d.ts delete mode 100644 node_modules/@types/node/zlib.d.ts delete mode 100644 node_modules/accepts/HISTORY.md delete mode 100644 node_modules/accepts/LICENSE delete mode 100644 node_modules/accepts/README.md delete mode 100644 node_modules/accepts/index.js delete mode 100644 node_modules/accepts/package.json delete mode 100644 node_modules/anymatch/LICENSE delete mode 100644 node_modules/anymatch/README.md delete mode 100644 node_modules/anymatch/index.d.ts delete mode 100644 node_modules/anymatch/index.js delete mode 100644 node_modules/anymatch/package.json delete mode 100644 node_modules/array-flatten/LICENSE delete mode 100644 node_modules/array-flatten/README.md delete mode 100644 node_modules/array-flatten/array-flatten.js delete mode 100644 node_modules/array-flatten/package.json delete mode 100644 node_modules/balanced-match/.github/FUNDING.yml delete mode 100644 node_modules/balanced-match/LICENSE.md delete mode 100644 node_modules/balanced-match/README.md delete mode 100644 node_modules/balanced-match/index.js delete mode 100644 node_modules/balanced-match/package.json delete mode 100644 node_modules/base64id/CHANGELOG.md delete mode 100644 node_modules/base64id/LICENSE delete mode 100644 node_modules/base64id/README.md delete mode 100644 node_modules/base64id/lib/base64id.js delete mode 100644 node_modules/base64id/package.json delete mode 100644 node_modules/binary-extensions/binary-extensions.json delete mode 100644 node_modules/binary-extensions/binary-extensions.json.d.ts delete mode 100644 node_modules/binary-extensions/index.d.ts delete mode 100644 node_modules/binary-extensions/index.js delete mode 100644 node_modules/binary-extensions/license delete mode 100644 node_modules/binary-extensions/package.json delete mode 100644 node_modules/binary-extensions/readme.md delete mode 100644 node_modules/body-parser/HISTORY.md delete mode 100644 node_modules/body-parser/LICENSE delete mode 100644 node_modules/body-parser/README.md delete mode 100644 node_modules/body-parser/SECURITY.md delete mode 100644 node_modules/body-parser/index.js delete mode 100644 node_modules/body-parser/lib/read.js delete mode 100644 node_modules/body-parser/lib/types/json.js delete mode 100644 node_modules/body-parser/lib/types/raw.js delete mode 100644 node_modules/body-parser/lib/types/text.js delete mode 100644 node_modules/body-parser/lib/types/urlencoded.js delete mode 100644 node_modules/body-parser/package.json delete mode 100644 node_modules/brace-expansion/LICENSE delete mode 100644 node_modules/brace-expansion/README.md delete mode 100644 node_modules/brace-expansion/index.js delete mode 100644 node_modules/brace-expansion/package.json delete mode 100644 node_modules/braces/LICENSE delete mode 100644 node_modules/braces/README.md delete mode 100644 node_modules/braces/index.js delete mode 100644 node_modules/braces/lib/compile.js delete mode 100644 node_modules/braces/lib/constants.js delete mode 100644 node_modules/braces/lib/expand.js delete mode 100644 node_modules/braces/lib/parse.js delete mode 100644 node_modules/braces/lib/stringify.js delete mode 100644 node_modules/braces/lib/utils.js delete mode 100644 node_modules/braces/package.json delete mode 100644 node_modules/bytes/History.md delete mode 100644 node_modules/bytes/LICENSE delete mode 100644 node_modules/bytes/Readme.md delete mode 100644 node_modules/bytes/index.js delete mode 100644 node_modules/bytes/package.json delete mode 100644 node_modules/call-bind-apply-helpers/.eslintrc delete mode 100644 node_modules/call-bind-apply-helpers/.github/FUNDING.yml delete mode 100644 node_modules/call-bind-apply-helpers/.nycrc delete mode 100644 node_modules/call-bind-apply-helpers/CHANGELOG.md delete mode 100644 node_modules/call-bind-apply-helpers/LICENSE delete mode 100644 node_modules/call-bind-apply-helpers/README.md delete mode 100644 node_modules/call-bind-apply-helpers/actualApply.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/actualApply.js delete mode 100644 node_modules/call-bind-apply-helpers/applyBind.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/applyBind.js delete mode 100644 node_modules/call-bind-apply-helpers/functionApply.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/functionApply.js delete mode 100644 node_modules/call-bind-apply-helpers/functionCall.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/functionCall.js delete mode 100644 node_modules/call-bind-apply-helpers/index.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/index.js delete mode 100644 node_modules/call-bind-apply-helpers/package.json delete mode 100644 node_modules/call-bind-apply-helpers/reflectApply.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/reflectApply.js delete mode 100644 node_modules/call-bind-apply-helpers/test/index.js delete mode 100644 node_modules/call-bind-apply-helpers/tsconfig.json delete mode 100644 node_modules/call-bound/.eslintrc delete mode 100644 node_modules/call-bound/.github/FUNDING.yml delete mode 100644 node_modules/call-bound/.nycrc delete mode 100644 node_modules/call-bound/CHANGELOG.md delete mode 100644 node_modules/call-bound/LICENSE delete mode 100644 node_modules/call-bound/README.md delete mode 100644 node_modules/call-bound/index.d.ts delete mode 100644 node_modules/call-bound/index.js delete mode 100644 node_modules/call-bound/package.json delete mode 100644 node_modules/call-bound/test/index.js delete mode 100644 node_modules/call-bound/tsconfig.json delete mode 100644 node_modules/chokidar/LICENSE delete mode 100644 node_modules/chokidar/README.md delete mode 100644 node_modules/chokidar/index.js delete mode 100644 node_modules/chokidar/lib/constants.js delete mode 100644 node_modules/chokidar/lib/fsevents-handler.js delete mode 100644 node_modules/chokidar/lib/nodefs-handler.js delete mode 100644 node_modules/chokidar/package.json delete mode 100644 node_modules/chokidar/types/index.d.ts delete mode 100644 node_modules/concat-map/.travis.yml delete mode 100644 node_modules/concat-map/LICENSE delete mode 100644 node_modules/concat-map/README.markdown delete mode 100644 node_modules/concat-map/example/map.js delete mode 100644 node_modules/concat-map/index.js delete mode 100644 node_modules/concat-map/package.json delete mode 100644 node_modules/concat-map/test/map.js delete mode 100644 node_modules/content-disposition/HISTORY.md delete mode 100644 node_modules/content-disposition/LICENSE delete mode 100644 node_modules/content-disposition/README.md delete mode 100644 node_modules/content-disposition/index.js delete mode 100644 node_modules/content-disposition/package.json delete mode 100644 node_modules/content-type/HISTORY.md delete mode 100644 node_modules/content-type/LICENSE delete mode 100644 node_modules/content-type/README.md delete mode 100644 node_modules/content-type/index.js delete mode 100644 node_modules/content-type/package.json delete mode 100644 node_modules/cookie-signature/.npmignore delete mode 100644 node_modules/cookie-signature/History.md delete mode 100644 node_modules/cookie-signature/Readme.md delete mode 100644 node_modules/cookie-signature/index.js delete mode 100644 node_modules/cookie-signature/package.json delete mode 100644 node_modules/cookie/LICENSE delete mode 100644 node_modules/cookie/README.md delete mode 100644 node_modules/cookie/SECURITY.md delete mode 100644 node_modules/cookie/index.js delete mode 100644 node_modules/cookie/package.json delete mode 100644 node_modules/cors/CONTRIBUTING.md delete mode 100644 node_modules/cors/HISTORY.md delete mode 100644 node_modules/cors/LICENSE delete mode 100644 node_modules/cors/README.md delete mode 100644 node_modules/cors/lib/index.js delete mode 100644 node_modules/cors/package.json delete mode 100644 node_modules/debug/.coveralls.yml delete mode 100644 node_modules/debug/.eslintrc delete mode 100644 node_modules/debug/.npmignore delete mode 100644 node_modules/debug/.travis.yml delete mode 100644 node_modules/debug/CHANGELOG.md delete mode 100644 node_modules/debug/LICENSE delete mode 100644 node_modules/debug/Makefile delete mode 100644 node_modules/debug/README.md delete mode 100644 node_modules/debug/component.json delete mode 100644 node_modules/debug/karma.conf.js delete mode 100644 node_modules/debug/node.js delete mode 100644 node_modules/debug/package.json delete mode 100644 node_modules/debug/src/browser.js delete mode 100644 node_modules/debug/src/debug.js delete mode 100644 node_modules/debug/src/index.js delete mode 100644 node_modules/debug/src/inspector-log.js delete mode 100644 node_modules/debug/src/node.js delete mode 100644 node_modules/depd/History.md delete mode 100644 node_modules/depd/LICENSE delete mode 100644 node_modules/depd/Readme.md delete mode 100644 node_modules/depd/index.js delete mode 100644 node_modules/depd/lib/browser/index.js delete mode 100644 node_modules/depd/package.json delete mode 100644 node_modules/destroy/LICENSE delete mode 100644 node_modules/destroy/README.md delete mode 100644 node_modules/destroy/index.js delete mode 100644 node_modules/destroy/package.json delete mode 100644 node_modules/dunder-proto/.eslintrc delete mode 100644 node_modules/dunder-proto/.github/FUNDING.yml delete mode 100644 node_modules/dunder-proto/.nycrc delete mode 100644 node_modules/dunder-proto/CHANGELOG.md delete mode 100644 node_modules/dunder-proto/LICENSE delete mode 100644 node_modules/dunder-proto/README.md delete mode 100644 node_modules/dunder-proto/get.d.ts delete mode 100644 node_modules/dunder-proto/get.js delete mode 100644 node_modules/dunder-proto/package.json delete mode 100644 node_modules/dunder-proto/set.d.ts delete mode 100644 node_modules/dunder-proto/set.js delete mode 100644 node_modules/dunder-proto/test/get.js delete mode 100644 node_modules/dunder-proto/test/index.js delete mode 100644 node_modules/dunder-proto/test/set.js delete mode 100644 node_modules/dunder-proto/tsconfig.json delete mode 100644 node_modules/ee-first/LICENSE delete mode 100644 node_modules/ee-first/README.md delete mode 100644 node_modules/ee-first/index.js delete mode 100644 node_modules/ee-first/package.json delete mode 100644 node_modules/encodeurl/LICENSE delete mode 100644 node_modules/encodeurl/README.md delete mode 100644 node_modules/encodeurl/index.js delete mode 100644 node_modules/encodeurl/package.json delete mode 100644 node_modules/engine.io-parser/LICENSE delete mode 100644 node_modules/engine.io-parser/Readme.md delete mode 100644 node_modules/engine.io-parser/build/cjs/commons.d.ts delete mode 100644 node_modules/engine.io-parser/build/cjs/commons.js delete mode 100644 node_modules/engine.io-parser/build/cjs/contrib/base64-arraybuffer.d.ts delete mode 100644 node_modules/engine.io-parser/build/cjs/contrib/base64-arraybuffer.js delete mode 100644 node_modules/engine.io-parser/build/cjs/decodePacket.browser.d.ts delete mode 100644 node_modules/engine.io-parser/build/cjs/decodePacket.browser.js delete mode 100644 node_modules/engine.io-parser/build/cjs/decodePacket.d.ts delete mode 100644 node_modules/engine.io-parser/build/cjs/decodePacket.js delete mode 100644 node_modules/engine.io-parser/build/cjs/encodePacket.browser.d.ts delete mode 100644 node_modules/engine.io-parser/build/cjs/encodePacket.browser.js delete mode 100644 node_modules/engine.io-parser/build/cjs/encodePacket.d.ts delete mode 100644 node_modules/engine.io-parser/build/cjs/encodePacket.js delete mode 100644 node_modules/engine.io-parser/build/cjs/index.d.ts delete mode 100644 node_modules/engine.io-parser/build/cjs/index.js delete mode 100644 node_modules/engine.io-parser/build/cjs/package.json delete mode 100644 node_modules/engine.io-parser/build/esm/commons.d.ts delete mode 100644 node_modules/engine.io-parser/build/esm/commons.js delete mode 100644 node_modules/engine.io-parser/build/esm/contrib/base64-arraybuffer.d.ts delete mode 100644 node_modules/engine.io-parser/build/esm/contrib/base64-arraybuffer.js delete mode 100644 node_modules/engine.io-parser/build/esm/decodePacket.browser.d.ts delete mode 100644 node_modules/engine.io-parser/build/esm/decodePacket.browser.js delete mode 100644 node_modules/engine.io-parser/build/esm/decodePacket.d.ts delete mode 100644 node_modules/engine.io-parser/build/esm/decodePacket.js delete mode 100644 node_modules/engine.io-parser/build/esm/encodePacket.browser.d.ts delete mode 100644 node_modules/engine.io-parser/build/esm/encodePacket.browser.js delete mode 100644 node_modules/engine.io-parser/build/esm/encodePacket.d.ts delete mode 100644 node_modules/engine.io-parser/build/esm/encodePacket.js delete mode 100644 node_modules/engine.io-parser/build/esm/index.d.ts delete mode 100644 node_modules/engine.io-parser/build/esm/index.js delete mode 100644 node_modules/engine.io-parser/build/esm/package.json delete mode 100644 node_modules/engine.io-parser/package.json delete mode 100644 node_modules/engine.io/LICENSE delete mode 100644 node_modules/engine.io/README.md delete mode 100644 node_modules/engine.io/build/contrib/types.cookie.d.ts delete mode 100644 node_modules/engine.io/build/contrib/types.cookie.js delete mode 100644 node_modules/engine.io/build/engine.io.d.ts delete mode 100644 node_modules/engine.io/build/engine.io.js delete mode 100644 node_modules/engine.io/build/parser-v3/index.d.ts delete mode 100644 node_modules/engine.io/build/parser-v3/index.js delete mode 100644 node_modules/engine.io/build/parser-v3/utf8.d.ts delete mode 100644 node_modules/engine.io/build/parser-v3/utf8.js delete mode 100644 node_modules/engine.io/build/server.d.ts delete mode 100644 node_modules/engine.io/build/server.js delete mode 100644 node_modules/engine.io/build/socket.d.ts delete mode 100644 node_modules/engine.io/build/socket.js delete mode 100644 node_modules/engine.io/build/transport.d.ts delete mode 100644 node_modules/engine.io/build/transport.js delete mode 100644 node_modules/engine.io/build/transports-uws/index.d.ts delete mode 100644 node_modules/engine.io/build/transports-uws/index.js delete mode 100644 node_modules/engine.io/build/transports-uws/polling.d.ts delete mode 100644 node_modules/engine.io/build/transports-uws/polling.js delete mode 100644 node_modules/engine.io/build/transports-uws/websocket.d.ts delete mode 100644 node_modules/engine.io/build/transports-uws/websocket.js delete mode 100644 node_modules/engine.io/build/transports/index.d.ts delete mode 100644 node_modules/engine.io/build/transports/index.js delete mode 100644 node_modules/engine.io/build/transports/polling-jsonp.d.ts delete mode 100644 node_modules/engine.io/build/transports/polling-jsonp.js delete mode 100644 node_modules/engine.io/build/transports/polling.d.ts delete mode 100644 node_modules/engine.io/build/transports/polling.js delete mode 100644 node_modules/engine.io/build/transports/websocket.d.ts delete mode 100644 node_modules/engine.io/build/transports/websocket.js delete mode 100644 node_modules/engine.io/build/transports/webtransport.d.ts delete mode 100644 node_modules/engine.io/build/transports/webtransport.js delete mode 100644 node_modules/engine.io/build/userver.d.ts delete mode 100644 node_modules/engine.io/build/userver.js delete mode 100644 node_modules/engine.io/node_modules/cookie/LICENSE delete mode 100644 node_modules/engine.io/node_modules/cookie/README.md delete mode 100644 node_modules/engine.io/node_modules/cookie/SECURITY.md delete mode 100644 node_modules/engine.io/node_modules/cookie/index.js delete mode 100644 node_modules/engine.io/node_modules/cookie/package.json delete mode 100644 node_modules/engine.io/node_modules/debug/LICENSE delete mode 100644 node_modules/engine.io/node_modules/debug/README.md delete mode 100644 node_modules/engine.io/node_modules/debug/package.json delete mode 100644 node_modules/engine.io/node_modules/debug/src/browser.js delete mode 100644 node_modules/engine.io/node_modules/debug/src/common.js delete mode 100644 node_modules/engine.io/node_modules/debug/src/index.js delete mode 100644 node_modules/engine.io/node_modules/debug/src/node.js delete mode 100644 node_modules/engine.io/node_modules/ms/index.js delete mode 100644 node_modules/engine.io/node_modules/ms/license.md delete mode 100644 node_modules/engine.io/node_modules/ms/package.json delete mode 100644 node_modules/engine.io/node_modules/ms/readme.md delete mode 100644 node_modules/engine.io/package.json delete mode 100644 node_modules/engine.io/wrapper.mjs delete mode 100644 node_modules/es-define-property/.eslintrc delete mode 100644 node_modules/es-define-property/.github/FUNDING.yml delete mode 100644 node_modules/es-define-property/.nycrc delete mode 100644 node_modules/es-define-property/CHANGELOG.md delete mode 100644 node_modules/es-define-property/LICENSE delete mode 100644 node_modules/es-define-property/README.md delete mode 100644 node_modules/es-define-property/index.d.ts delete mode 100644 node_modules/es-define-property/index.js delete mode 100644 node_modules/es-define-property/package.json delete mode 100644 node_modules/es-define-property/test/index.js delete mode 100644 node_modules/es-define-property/tsconfig.json delete mode 100644 node_modules/es-errors/.eslintrc delete mode 100644 node_modules/es-errors/.github/FUNDING.yml delete mode 100644 node_modules/es-errors/CHANGELOG.md delete mode 100644 node_modules/es-errors/LICENSE delete mode 100644 node_modules/es-errors/README.md delete mode 100644 node_modules/es-errors/eval.d.ts delete mode 100644 node_modules/es-errors/eval.js delete mode 100644 node_modules/es-errors/index.d.ts delete mode 100644 node_modules/es-errors/index.js delete mode 100644 node_modules/es-errors/package.json delete mode 100644 node_modules/es-errors/range.d.ts delete mode 100644 node_modules/es-errors/range.js delete mode 100644 node_modules/es-errors/ref.d.ts delete mode 100644 node_modules/es-errors/ref.js delete mode 100644 node_modules/es-errors/syntax.d.ts delete mode 100644 node_modules/es-errors/syntax.js delete mode 100644 node_modules/es-errors/test/index.js delete mode 100644 node_modules/es-errors/tsconfig.json delete mode 100644 node_modules/es-errors/type.d.ts delete mode 100644 node_modules/es-errors/type.js delete mode 100644 node_modules/es-errors/uri.d.ts delete mode 100644 node_modules/es-errors/uri.js delete mode 100644 node_modules/es-object-atoms/.eslintrc delete mode 100644 node_modules/es-object-atoms/.github/FUNDING.yml delete mode 100644 node_modules/es-object-atoms/CHANGELOG.md delete mode 100644 node_modules/es-object-atoms/LICENSE delete mode 100644 node_modules/es-object-atoms/README.md delete mode 100644 node_modules/es-object-atoms/RequireObjectCoercible.d.ts delete mode 100644 node_modules/es-object-atoms/RequireObjectCoercible.js delete mode 100644 node_modules/es-object-atoms/ToObject.d.ts delete mode 100644 node_modules/es-object-atoms/ToObject.js delete mode 100644 node_modules/es-object-atoms/index.d.ts delete mode 100644 node_modules/es-object-atoms/index.js delete mode 100644 node_modules/es-object-atoms/isObject.d.ts delete mode 100644 node_modules/es-object-atoms/isObject.js delete mode 100644 node_modules/es-object-atoms/package.json delete mode 100644 node_modules/es-object-atoms/test/index.js delete mode 100644 node_modules/es-object-atoms/tsconfig.json delete mode 100644 node_modules/escape-html/LICENSE delete mode 100644 node_modules/escape-html/Readme.md delete mode 100644 node_modules/escape-html/index.js delete mode 100644 node_modules/escape-html/package.json delete mode 100644 node_modules/etag/HISTORY.md delete mode 100644 node_modules/etag/LICENSE delete mode 100644 node_modules/etag/README.md delete mode 100644 node_modules/etag/index.js delete mode 100644 node_modules/etag/package.json delete mode 100644 node_modules/express/History.md delete mode 100644 node_modules/express/LICENSE delete mode 100644 node_modules/express/Readme.md delete mode 100644 node_modules/express/index.js delete mode 100644 node_modules/express/lib/application.js delete mode 100644 node_modules/express/lib/express.js delete mode 100644 node_modules/express/lib/middleware/init.js delete mode 100644 node_modules/express/lib/middleware/query.js delete mode 100644 node_modules/express/lib/request.js delete mode 100644 node_modules/express/lib/response.js delete mode 100644 node_modules/express/lib/router/index.js delete mode 100644 node_modules/express/lib/router/layer.js delete mode 100644 node_modules/express/lib/router/route.js delete mode 100644 node_modules/express/lib/utils.js delete mode 100644 node_modules/express/lib/view.js delete mode 100644 node_modules/express/package.json delete mode 100644 node_modules/fill-range/LICENSE delete mode 100644 node_modules/fill-range/README.md delete mode 100644 node_modules/fill-range/index.js delete mode 100644 node_modules/fill-range/package.json delete mode 100644 node_modules/finalhandler/HISTORY.md delete mode 100644 node_modules/finalhandler/LICENSE delete mode 100644 node_modules/finalhandler/README.md delete mode 100644 node_modules/finalhandler/SECURITY.md delete mode 100644 node_modules/finalhandler/index.js delete mode 100644 node_modules/finalhandler/package.json delete mode 100644 node_modules/forwarded/HISTORY.md delete mode 100644 node_modules/forwarded/LICENSE delete mode 100644 node_modules/forwarded/README.md delete mode 100644 node_modules/forwarded/index.js delete mode 100644 node_modules/forwarded/package.json delete mode 100644 node_modules/fresh/HISTORY.md delete mode 100644 node_modules/fresh/LICENSE delete mode 100644 node_modules/fresh/README.md delete mode 100644 node_modules/fresh/index.js delete mode 100644 node_modules/fresh/package.json delete mode 100644 node_modules/function-bind/.eslintrc delete mode 100644 node_modules/function-bind/.github/FUNDING.yml delete mode 100644 node_modules/function-bind/.github/SECURITY.md delete mode 100644 node_modules/function-bind/.nycrc delete mode 100644 node_modules/function-bind/CHANGELOG.md delete mode 100644 node_modules/function-bind/LICENSE delete mode 100644 node_modules/function-bind/README.md delete mode 100644 node_modules/function-bind/implementation.js delete mode 100644 node_modules/function-bind/index.js delete mode 100644 node_modules/function-bind/package.json delete mode 100644 node_modules/function-bind/test/.eslintrc delete mode 100644 node_modules/function-bind/test/index.js delete mode 100644 node_modules/get-intrinsic/.eslintrc delete mode 100644 node_modules/get-intrinsic/.github/FUNDING.yml delete mode 100644 node_modules/get-intrinsic/.nycrc delete mode 100644 node_modules/get-intrinsic/CHANGELOG.md delete mode 100644 node_modules/get-intrinsic/LICENSE delete mode 100644 node_modules/get-intrinsic/README.md delete mode 100644 node_modules/get-intrinsic/index.js delete mode 100644 node_modules/get-intrinsic/package.json delete mode 100644 node_modules/get-intrinsic/test/GetIntrinsic.js delete mode 100644 node_modules/get-proto/.eslintrc delete mode 100644 node_modules/get-proto/.github/FUNDING.yml delete mode 100644 node_modules/get-proto/.nycrc delete mode 100644 node_modules/get-proto/CHANGELOG.md delete mode 100644 node_modules/get-proto/LICENSE delete mode 100644 node_modules/get-proto/Object.getPrototypeOf.d.ts delete mode 100644 node_modules/get-proto/Object.getPrototypeOf.js delete mode 100644 node_modules/get-proto/README.md delete mode 100644 node_modules/get-proto/Reflect.getPrototypeOf.d.ts delete mode 100644 node_modules/get-proto/Reflect.getPrototypeOf.js delete mode 100644 node_modules/get-proto/index.d.ts delete mode 100644 node_modules/get-proto/index.js delete mode 100644 node_modules/get-proto/package.json delete mode 100644 node_modules/get-proto/test/index.js delete mode 100644 node_modules/get-proto/tsconfig.json delete mode 100644 node_modules/glob-parent/CHANGELOG.md delete mode 100644 node_modules/glob-parent/LICENSE delete mode 100644 node_modules/glob-parent/README.md delete mode 100644 node_modules/glob-parent/index.js delete mode 100644 node_modules/glob-parent/package.json delete mode 100644 node_modules/gopd/.eslintrc delete mode 100644 node_modules/gopd/.github/FUNDING.yml delete mode 100644 node_modules/gopd/CHANGELOG.md delete mode 100644 node_modules/gopd/LICENSE delete mode 100644 node_modules/gopd/README.md delete mode 100644 node_modules/gopd/gOPD.d.ts delete mode 100644 node_modules/gopd/gOPD.js delete mode 100644 node_modules/gopd/index.d.ts delete mode 100644 node_modules/gopd/index.js delete mode 100644 node_modules/gopd/package.json delete mode 100644 node_modules/gopd/test/index.js delete mode 100644 node_modules/gopd/tsconfig.json delete mode 100644 node_modules/has-flag/index.js delete mode 100644 node_modules/has-flag/license delete mode 100644 node_modules/has-flag/package.json delete mode 100644 node_modules/has-flag/readme.md delete mode 100644 node_modules/has-symbols/.eslintrc delete mode 100644 node_modules/has-symbols/.github/FUNDING.yml delete mode 100644 node_modules/has-symbols/.nycrc delete mode 100644 node_modules/has-symbols/CHANGELOG.md delete mode 100644 node_modules/has-symbols/LICENSE delete mode 100644 node_modules/has-symbols/README.md delete mode 100644 node_modules/has-symbols/index.d.ts delete mode 100644 node_modules/has-symbols/index.js delete mode 100644 node_modules/has-symbols/package.json delete mode 100644 node_modules/has-symbols/shams.d.ts delete mode 100644 node_modules/has-symbols/shams.js delete mode 100644 node_modules/has-symbols/test/index.js delete mode 100644 node_modules/has-symbols/test/shams/core-js.js delete mode 100644 node_modules/has-symbols/test/shams/get-own-property-symbols.js delete mode 100644 node_modules/has-symbols/test/tests.js delete mode 100644 node_modules/has-symbols/tsconfig.json delete mode 100644 node_modules/hasown/.eslintrc delete mode 100644 node_modules/hasown/.github/FUNDING.yml delete mode 100644 node_modules/hasown/.nycrc delete mode 100644 node_modules/hasown/CHANGELOG.md delete mode 100644 node_modules/hasown/LICENSE delete mode 100644 node_modules/hasown/README.md delete mode 100644 node_modules/hasown/index.d.ts delete mode 100644 node_modules/hasown/index.js delete mode 100644 node_modules/hasown/package.json delete mode 100644 node_modules/hasown/tsconfig.json delete mode 100644 node_modules/http-errors/HISTORY.md delete mode 100644 node_modules/http-errors/LICENSE delete mode 100644 node_modules/http-errors/README.md delete mode 100644 node_modules/http-errors/index.js delete mode 100644 node_modules/http-errors/package.json delete mode 100644 node_modules/iconv-lite/Changelog.md delete mode 100644 node_modules/iconv-lite/LICENSE delete mode 100644 node_modules/iconv-lite/README.md delete mode 100644 node_modules/iconv-lite/encodings/dbcs-codec.js delete mode 100644 node_modules/iconv-lite/encodings/dbcs-data.js delete mode 100644 node_modules/iconv-lite/encodings/index.js delete mode 100644 node_modules/iconv-lite/encodings/internal.js delete mode 100644 node_modules/iconv-lite/encodings/sbcs-codec.js delete mode 100644 node_modules/iconv-lite/encodings/sbcs-data-generated.js delete mode 100644 node_modules/iconv-lite/encodings/sbcs-data.js delete mode 100644 node_modules/iconv-lite/encodings/tables/big5-added.json delete mode 100644 node_modules/iconv-lite/encodings/tables/cp936.json delete mode 100644 node_modules/iconv-lite/encodings/tables/cp949.json delete mode 100644 node_modules/iconv-lite/encodings/tables/cp950.json delete mode 100644 node_modules/iconv-lite/encodings/tables/eucjp.json delete mode 100644 node_modules/iconv-lite/encodings/tables/gb18030-ranges.json delete mode 100644 node_modules/iconv-lite/encodings/tables/gbk-added.json delete mode 100644 node_modules/iconv-lite/encodings/tables/shiftjis.json delete mode 100644 node_modules/iconv-lite/encodings/utf16.js delete mode 100644 node_modules/iconv-lite/encodings/utf7.js delete mode 100644 node_modules/iconv-lite/lib/bom-handling.js delete mode 100644 node_modules/iconv-lite/lib/extend-node.js delete mode 100644 node_modules/iconv-lite/lib/index.d.ts delete mode 100644 node_modules/iconv-lite/lib/index.js delete mode 100644 node_modules/iconv-lite/lib/streams.js delete mode 100644 node_modules/iconv-lite/package.json delete mode 100644 node_modules/ignore-by-default/LICENSE delete mode 100644 node_modules/ignore-by-default/README.md delete mode 100644 node_modules/ignore-by-default/index.js delete mode 100644 node_modules/ignore-by-default/package.json delete mode 100644 node_modules/inherits/LICENSE delete mode 100644 node_modules/inherits/README.md delete mode 100644 node_modules/inherits/inherits.js delete mode 100644 node_modules/inherits/inherits_browser.js delete mode 100644 node_modules/inherits/package.json delete mode 100644 node_modules/ipaddr.js/LICENSE delete mode 100644 node_modules/ipaddr.js/README.md delete mode 100644 node_modules/ipaddr.js/ipaddr.min.js delete mode 100644 node_modules/ipaddr.js/lib/ipaddr.js delete mode 100644 node_modules/ipaddr.js/lib/ipaddr.js.d.ts delete mode 100644 node_modules/ipaddr.js/package.json delete mode 100644 node_modules/is-binary-path/index.d.ts delete mode 100644 node_modules/is-binary-path/index.js delete mode 100644 node_modules/is-binary-path/license delete mode 100644 node_modules/is-binary-path/package.json delete mode 100644 node_modules/is-binary-path/readme.md delete mode 100644 node_modules/is-extglob/LICENSE delete mode 100644 node_modules/is-extglob/README.md delete mode 100644 node_modules/is-extglob/index.js delete mode 100644 node_modules/is-extglob/package.json delete mode 100644 node_modules/is-glob/LICENSE delete mode 100644 node_modules/is-glob/README.md delete mode 100644 node_modules/is-glob/index.js delete mode 100644 node_modules/is-glob/package.json delete mode 100644 node_modules/is-number/LICENSE delete mode 100644 node_modules/is-number/README.md delete mode 100644 node_modules/is-number/index.js delete mode 100644 node_modules/is-number/package.json delete mode 100644 node_modules/math-intrinsics/.eslintrc delete mode 100644 node_modules/math-intrinsics/.github/FUNDING.yml delete mode 100644 node_modules/math-intrinsics/CHANGELOG.md delete mode 100644 node_modules/math-intrinsics/LICENSE delete mode 100644 node_modules/math-intrinsics/README.md delete mode 100644 node_modules/math-intrinsics/abs.d.ts delete mode 100644 node_modules/math-intrinsics/abs.js delete mode 100644 node_modules/math-intrinsics/constants/maxArrayLength.d.ts delete mode 100644 node_modules/math-intrinsics/constants/maxArrayLength.js delete mode 100644 node_modules/math-intrinsics/constants/maxSafeInteger.d.ts delete mode 100644 node_modules/math-intrinsics/constants/maxSafeInteger.js delete mode 100644 node_modules/math-intrinsics/constants/maxValue.d.ts delete mode 100644 node_modules/math-intrinsics/constants/maxValue.js delete mode 100644 node_modules/math-intrinsics/floor.d.ts delete mode 100644 node_modules/math-intrinsics/floor.js delete mode 100644 node_modules/math-intrinsics/isFinite.d.ts delete mode 100644 node_modules/math-intrinsics/isFinite.js delete mode 100644 node_modules/math-intrinsics/isInteger.d.ts delete mode 100644 node_modules/math-intrinsics/isInteger.js delete mode 100644 node_modules/math-intrinsics/isNaN.d.ts delete mode 100644 node_modules/math-intrinsics/isNaN.js delete mode 100644 node_modules/math-intrinsics/isNegativeZero.d.ts delete mode 100644 node_modules/math-intrinsics/isNegativeZero.js delete mode 100644 node_modules/math-intrinsics/max.d.ts delete mode 100644 node_modules/math-intrinsics/max.js delete mode 100644 node_modules/math-intrinsics/min.d.ts delete mode 100644 node_modules/math-intrinsics/min.js delete mode 100644 node_modules/math-intrinsics/mod.d.ts delete mode 100644 node_modules/math-intrinsics/mod.js delete mode 100644 node_modules/math-intrinsics/package.json delete mode 100644 node_modules/math-intrinsics/pow.d.ts delete mode 100644 node_modules/math-intrinsics/pow.js delete mode 100644 node_modules/math-intrinsics/round.d.ts delete mode 100644 node_modules/math-intrinsics/round.js delete mode 100644 node_modules/math-intrinsics/sign.d.ts delete mode 100644 node_modules/math-intrinsics/sign.js delete mode 100644 node_modules/math-intrinsics/test/index.js delete mode 100644 node_modules/math-intrinsics/tsconfig.json delete mode 100644 node_modules/media-typer/HISTORY.md delete mode 100644 node_modules/media-typer/LICENSE delete mode 100644 node_modules/media-typer/README.md delete mode 100644 node_modules/media-typer/index.js delete mode 100644 node_modules/media-typer/package.json delete mode 100644 node_modules/merge-descriptors/HISTORY.md delete mode 100644 node_modules/merge-descriptors/LICENSE delete mode 100644 node_modules/merge-descriptors/README.md delete mode 100644 node_modules/merge-descriptors/index.js delete mode 100644 node_modules/merge-descriptors/package.json delete mode 100644 node_modules/methods/HISTORY.md delete mode 100644 node_modules/methods/LICENSE delete mode 100644 node_modules/methods/README.md delete mode 100644 node_modules/methods/index.js delete mode 100644 node_modules/methods/package.json delete mode 100644 node_modules/mime-db/HISTORY.md delete mode 100644 node_modules/mime-db/LICENSE delete mode 100644 node_modules/mime-db/README.md delete mode 100644 node_modules/mime-db/db.json delete mode 100644 node_modules/mime-db/index.js delete mode 100644 node_modules/mime-db/package.json delete mode 100644 node_modules/mime-types/HISTORY.md delete mode 100644 node_modules/mime-types/LICENSE delete mode 100644 node_modules/mime-types/README.md delete mode 100644 node_modules/mime-types/index.js delete mode 100644 node_modules/mime-types/package.json delete mode 100644 node_modules/mime/.npmignore delete mode 100644 node_modules/mime/CHANGELOG.md delete mode 100644 node_modules/mime/LICENSE delete mode 100644 node_modules/mime/README.md delete mode 100755 node_modules/mime/cli.js delete mode 100644 node_modules/mime/mime.js delete mode 100644 node_modules/mime/package.json delete mode 100755 node_modules/mime/src/build.js delete mode 100644 node_modules/mime/src/test.js delete mode 100644 node_modules/mime/types.json delete mode 100644 node_modules/minimatch/LICENSE delete mode 100644 node_modules/minimatch/README.md delete mode 100644 node_modules/minimatch/minimatch.js delete mode 100644 node_modules/minimatch/package.json delete mode 100644 node_modules/ms/index.js delete mode 100644 node_modules/ms/license.md delete mode 100644 node_modules/ms/package.json delete mode 100644 node_modules/ms/readme.md delete mode 100644 node_modules/negotiator/HISTORY.md delete mode 100644 node_modules/negotiator/LICENSE delete mode 100644 node_modules/negotiator/README.md delete mode 100644 node_modules/negotiator/index.js delete mode 100644 node_modules/negotiator/lib/charset.js delete mode 100644 node_modules/negotiator/lib/encoding.js delete mode 100644 node_modules/negotiator/lib/language.js delete mode 100644 node_modules/negotiator/lib/mediaType.js delete mode 100644 node_modules/negotiator/package.json delete mode 100644 node_modules/nodemon/.prettierrc.json delete mode 100644 node_modules/nodemon/LICENSE delete mode 100644 node_modules/nodemon/README.md delete mode 100755 node_modules/nodemon/bin/nodemon.js delete mode 100644 node_modules/nodemon/bin/windows-kill.exe delete mode 100644 node_modules/nodemon/doc/cli/authors.txt delete mode 100644 node_modules/nodemon/doc/cli/config.txt delete mode 100644 node_modules/nodemon/doc/cli/help.txt delete mode 100644 node_modules/nodemon/doc/cli/logo.txt delete mode 100644 node_modules/nodemon/doc/cli/options.txt delete mode 100644 node_modules/nodemon/doc/cli/topics.txt delete mode 100644 node_modules/nodemon/doc/cli/usage.txt delete mode 100644 node_modules/nodemon/doc/cli/whoami.txt delete mode 100644 node_modules/nodemon/index.d.ts delete mode 100644 node_modules/nodemon/jsconfig.json delete mode 100644 node_modules/nodemon/lib/cli/index.js delete mode 100644 node_modules/nodemon/lib/cli/parse.js delete mode 100644 node_modules/nodemon/lib/config/command.js delete mode 100644 node_modules/nodemon/lib/config/defaults.js delete mode 100644 node_modules/nodemon/lib/config/exec.js delete mode 100644 node_modules/nodemon/lib/config/index.js delete mode 100644 node_modules/nodemon/lib/config/load.js delete mode 100644 node_modules/nodemon/lib/help/index.js delete mode 100644 node_modules/nodemon/lib/index.js delete mode 100644 node_modules/nodemon/lib/monitor/index.js delete mode 100644 node_modules/nodemon/lib/monitor/match.js delete mode 100644 node_modules/nodemon/lib/monitor/run.js delete mode 100644 node_modules/nodemon/lib/monitor/signals.js delete mode 100644 node_modules/nodemon/lib/monitor/watch.js delete mode 100644 node_modules/nodemon/lib/nodemon.js delete mode 100644 node_modules/nodemon/lib/rules/add.js delete mode 100644 node_modules/nodemon/lib/rules/index.js delete mode 100644 node_modules/nodemon/lib/rules/parse.js delete mode 100644 node_modules/nodemon/lib/spawn.js delete mode 100644 node_modules/nodemon/lib/utils/bus.js delete mode 100644 node_modules/nodemon/lib/utils/clone.js delete mode 100644 node_modules/nodemon/lib/utils/colour.js delete mode 100644 node_modules/nodemon/lib/utils/index.js delete mode 100644 node_modules/nodemon/lib/utils/log.js delete mode 100644 node_modules/nodemon/lib/utils/merge.js delete mode 100644 node_modules/nodemon/lib/version.js delete mode 100644 node_modules/nodemon/node_modules/debug/LICENSE delete mode 100644 node_modules/nodemon/node_modules/debug/README.md delete mode 100644 node_modules/nodemon/node_modules/debug/package.json delete mode 100644 node_modules/nodemon/node_modules/debug/src/browser.js delete mode 100644 node_modules/nodemon/node_modules/debug/src/common.js delete mode 100644 node_modules/nodemon/node_modules/debug/src/index.js delete mode 100644 node_modules/nodemon/node_modules/debug/src/node.js delete mode 100644 node_modules/nodemon/node_modules/ms/index.js delete mode 100644 node_modules/nodemon/node_modules/ms/license.md delete mode 100644 node_modules/nodemon/node_modules/ms/package.json delete mode 100644 node_modules/nodemon/node_modules/ms/readme.md delete mode 100644 node_modules/nodemon/package.json delete mode 100644 node_modules/normalize-path/LICENSE delete mode 100644 node_modules/normalize-path/README.md delete mode 100644 node_modules/normalize-path/index.js delete mode 100644 node_modules/normalize-path/package.json delete mode 100644 node_modules/object-assign/index.js delete mode 100644 node_modules/object-assign/license delete mode 100644 node_modules/object-assign/package.json delete mode 100644 node_modules/object-assign/readme.md delete mode 100644 node_modules/object-inspect/.eslintrc delete mode 100644 node_modules/object-inspect/.github/FUNDING.yml delete mode 100644 node_modules/object-inspect/.nycrc delete mode 100644 node_modules/object-inspect/CHANGELOG.md delete mode 100644 node_modules/object-inspect/LICENSE delete mode 100644 node_modules/object-inspect/example/all.js delete mode 100644 node_modules/object-inspect/example/circular.js delete mode 100644 node_modules/object-inspect/example/fn.js delete mode 100644 node_modules/object-inspect/example/inspect.js delete mode 100644 node_modules/object-inspect/index.js delete mode 100644 node_modules/object-inspect/package-support.json delete mode 100644 node_modules/object-inspect/package.json delete mode 100644 node_modules/object-inspect/readme.markdown delete mode 100644 node_modules/object-inspect/test-core-js.js delete mode 100644 node_modules/object-inspect/test/bigint.js delete mode 100644 node_modules/object-inspect/test/browser/dom.js delete mode 100644 node_modules/object-inspect/test/circular.js delete mode 100644 node_modules/object-inspect/test/deep.js delete mode 100644 node_modules/object-inspect/test/element.js delete mode 100644 node_modules/object-inspect/test/err.js delete mode 100644 node_modules/object-inspect/test/fakes.js delete mode 100644 node_modules/object-inspect/test/fn.js delete mode 100644 node_modules/object-inspect/test/global.js delete mode 100644 node_modules/object-inspect/test/has.js delete mode 100644 node_modules/object-inspect/test/holes.js delete mode 100644 node_modules/object-inspect/test/indent-option.js delete mode 100644 node_modules/object-inspect/test/inspect.js delete mode 100644 node_modules/object-inspect/test/lowbyte.js delete mode 100644 node_modules/object-inspect/test/number.js delete mode 100644 node_modules/object-inspect/test/quoteStyle.js delete mode 100644 node_modules/object-inspect/test/toStringTag.js delete mode 100644 node_modules/object-inspect/test/undef.js delete mode 100644 node_modules/object-inspect/test/values.js delete mode 100644 node_modules/object-inspect/util.inspect.js delete mode 100644 node_modules/on-finished/HISTORY.md delete mode 100644 node_modules/on-finished/LICENSE delete mode 100644 node_modules/on-finished/README.md delete mode 100644 node_modules/on-finished/index.js delete mode 100644 node_modules/on-finished/package.json delete mode 100644 node_modules/parseurl/HISTORY.md delete mode 100644 node_modules/parseurl/LICENSE delete mode 100644 node_modules/parseurl/README.md delete mode 100644 node_modules/parseurl/index.js delete mode 100644 node_modules/parseurl/package.json delete mode 100644 node_modules/path-to-regexp/LICENSE delete mode 100644 node_modules/path-to-regexp/Readme.md delete mode 100644 node_modules/path-to-regexp/index.js delete mode 100644 node_modules/path-to-regexp/package.json delete mode 100644 node_modules/picomatch/CHANGELOG.md delete mode 100644 node_modules/picomatch/LICENSE delete mode 100644 node_modules/picomatch/README.md delete mode 100644 node_modules/picomatch/index.js delete mode 100644 node_modules/picomatch/lib/constants.js delete mode 100644 node_modules/picomatch/lib/parse.js delete mode 100644 node_modules/picomatch/lib/picomatch.js delete mode 100644 node_modules/picomatch/lib/scan.js delete mode 100644 node_modules/picomatch/lib/utils.js delete mode 100644 node_modules/picomatch/package.json delete mode 100644 node_modules/proxy-addr/HISTORY.md delete mode 100644 node_modules/proxy-addr/LICENSE delete mode 100644 node_modules/proxy-addr/README.md delete mode 100644 node_modules/proxy-addr/index.js delete mode 100644 node_modules/proxy-addr/package.json delete mode 100644 node_modules/pstree.remy/.travis.yml delete mode 100644 node_modules/pstree.remy/LICENSE delete mode 100644 node_modules/pstree.remy/README.md delete mode 100644 node_modules/pstree.remy/lib/index.js delete mode 100644 node_modules/pstree.remy/lib/tree.js delete mode 100644 node_modules/pstree.remy/lib/utils.js delete mode 100644 node_modules/pstree.remy/package.json delete mode 100644 node_modules/pstree.remy/tests/fixtures/index.js delete mode 100644 node_modules/pstree.remy/tests/fixtures/out1 delete mode 100644 node_modules/pstree.remy/tests/fixtures/out2 delete mode 100644 node_modules/pstree.remy/tests/index.test.js delete mode 100644 node_modules/qs/.editorconfig delete mode 100644 node_modules/qs/.eslintrc delete mode 100644 node_modules/qs/.github/FUNDING.yml delete mode 100644 node_modules/qs/.nycrc delete mode 100644 node_modules/qs/CHANGELOG.md delete mode 100644 node_modules/qs/LICENSE.md delete mode 100644 node_modules/qs/README.md delete mode 100644 node_modules/qs/dist/qs.js delete mode 100644 node_modules/qs/lib/formats.js delete mode 100644 node_modules/qs/lib/index.js delete mode 100644 node_modules/qs/lib/parse.js delete mode 100644 node_modules/qs/lib/stringify.js delete mode 100644 node_modules/qs/lib/utils.js delete mode 100644 node_modules/qs/package.json delete mode 100644 node_modules/qs/test/empty-keys-cases.js delete mode 100644 node_modules/qs/test/parse.js delete mode 100644 node_modules/qs/test/stringify.js delete mode 100644 node_modules/qs/test/utils.js delete mode 100644 node_modules/range-parser/HISTORY.md delete mode 100644 node_modules/range-parser/LICENSE delete mode 100644 node_modules/range-parser/README.md delete mode 100644 node_modules/range-parser/index.js delete mode 100644 node_modules/range-parser/package.json delete mode 100644 node_modules/raw-body/HISTORY.md delete mode 100644 node_modules/raw-body/LICENSE delete mode 100644 node_modules/raw-body/README.md delete mode 100644 node_modules/raw-body/SECURITY.md delete mode 100644 node_modules/raw-body/index.d.ts delete mode 100644 node_modules/raw-body/index.js delete mode 100644 node_modules/raw-body/package.json delete mode 100644 node_modules/readdirp/LICENSE delete mode 100644 node_modules/readdirp/README.md delete mode 100644 node_modules/readdirp/index.d.ts delete mode 100644 node_modules/readdirp/index.js delete mode 100644 node_modules/readdirp/package.json delete mode 100644 node_modules/safe-buffer/LICENSE delete mode 100644 node_modules/safe-buffer/README.md delete mode 100644 node_modules/safe-buffer/index.d.ts delete mode 100644 node_modules/safe-buffer/index.js delete mode 100644 node_modules/safe-buffer/package.json delete mode 100644 node_modules/safer-buffer/LICENSE delete mode 100644 node_modules/safer-buffer/Porting-Buffer.md delete mode 100644 node_modules/safer-buffer/Readme.md delete mode 100644 node_modules/safer-buffer/dangerous.js delete mode 100644 node_modules/safer-buffer/package.json delete mode 100644 node_modules/safer-buffer/safer.js delete mode 100644 node_modules/safer-buffer/tests.js delete mode 100644 node_modules/semver/LICENSE delete mode 100644 node_modules/semver/README.md delete mode 100755 node_modules/semver/bin/semver.js delete mode 100644 node_modules/semver/classes/comparator.js delete mode 100644 node_modules/semver/classes/index.js delete mode 100644 node_modules/semver/classes/range.js delete mode 100644 node_modules/semver/classes/semver.js delete mode 100644 node_modules/semver/functions/clean.js delete mode 100644 node_modules/semver/functions/cmp.js delete mode 100644 node_modules/semver/functions/coerce.js delete mode 100644 node_modules/semver/functions/compare-build.js delete mode 100644 node_modules/semver/functions/compare-loose.js delete mode 100644 node_modules/semver/functions/compare.js delete mode 100644 node_modules/semver/functions/diff.js delete mode 100644 node_modules/semver/functions/eq.js delete mode 100644 node_modules/semver/functions/gt.js delete mode 100644 node_modules/semver/functions/gte.js delete mode 100644 node_modules/semver/functions/inc.js delete mode 100644 node_modules/semver/functions/lt.js delete mode 100644 node_modules/semver/functions/lte.js delete mode 100644 node_modules/semver/functions/major.js delete mode 100644 node_modules/semver/functions/minor.js delete mode 100644 node_modules/semver/functions/neq.js delete mode 100644 node_modules/semver/functions/parse.js delete mode 100644 node_modules/semver/functions/patch.js delete mode 100644 node_modules/semver/functions/prerelease.js delete mode 100644 node_modules/semver/functions/rcompare.js delete mode 100644 node_modules/semver/functions/rsort.js delete mode 100644 node_modules/semver/functions/satisfies.js delete mode 100644 node_modules/semver/functions/sort.js delete mode 100644 node_modules/semver/functions/valid.js delete mode 100644 node_modules/semver/index.js delete mode 100644 node_modules/semver/internal/constants.js delete mode 100644 node_modules/semver/internal/debug.js delete mode 100644 node_modules/semver/internal/identifiers.js delete mode 100644 node_modules/semver/internal/lrucache.js delete mode 100644 node_modules/semver/internal/parse-options.js delete mode 100644 node_modules/semver/internal/re.js delete mode 100644 node_modules/semver/package.json delete mode 100644 node_modules/semver/preload.js delete mode 100644 node_modules/semver/range.bnf delete mode 100644 node_modules/semver/ranges/gtr.js delete mode 100644 node_modules/semver/ranges/intersects.js delete mode 100644 node_modules/semver/ranges/ltr.js delete mode 100644 node_modules/semver/ranges/max-satisfying.js delete mode 100644 node_modules/semver/ranges/min-satisfying.js delete mode 100644 node_modules/semver/ranges/min-version.js delete mode 100644 node_modules/semver/ranges/outside.js delete mode 100644 node_modules/semver/ranges/simplify.js delete mode 100644 node_modules/semver/ranges/subset.js delete mode 100644 node_modules/semver/ranges/to-comparators.js delete mode 100644 node_modules/semver/ranges/valid.js delete mode 100644 node_modules/send/HISTORY.md delete mode 100644 node_modules/send/LICENSE delete mode 100644 node_modules/send/README.md delete mode 100644 node_modules/send/SECURITY.md delete mode 100644 node_modules/send/index.js delete mode 100644 node_modules/send/node_modules/encodeurl/HISTORY.md delete mode 100644 node_modules/send/node_modules/encodeurl/LICENSE delete mode 100644 node_modules/send/node_modules/encodeurl/README.md delete mode 100644 node_modules/send/node_modules/encodeurl/index.js delete mode 100644 node_modules/send/node_modules/encodeurl/package.json delete mode 100644 node_modules/send/node_modules/ms/index.js delete mode 100644 node_modules/send/node_modules/ms/license.md delete mode 100644 node_modules/send/node_modules/ms/package.json delete mode 100644 node_modules/send/node_modules/ms/readme.md delete mode 100644 node_modules/send/package.json delete mode 100644 node_modules/serve-static/HISTORY.md delete mode 100644 node_modules/serve-static/LICENSE delete mode 100644 node_modules/serve-static/README.md delete mode 100644 node_modules/serve-static/index.js delete mode 100644 node_modules/serve-static/package.json delete mode 100644 node_modules/setprototypeof/LICENSE delete mode 100644 node_modules/setprototypeof/README.md delete mode 100644 node_modules/setprototypeof/index.d.ts delete mode 100644 node_modules/setprototypeof/index.js delete mode 100644 node_modules/setprototypeof/package.json delete mode 100644 node_modules/setprototypeof/test/index.js delete mode 100644 node_modules/side-channel-list/.editorconfig delete mode 100644 node_modules/side-channel-list/.eslintrc delete mode 100644 node_modules/side-channel-list/.github/FUNDING.yml delete mode 100644 node_modules/side-channel-list/.nycrc delete mode 100644 node_modules/side-channel-list/CHANGELOG.md delete mode 100644 node_modules/side-channel-list/LICENSE delete mode 100644 node_modules/side-channel-list/README.md delete mode 100644 node_modules/side-channel-list/index.d.ts delete mode 100644 node_modules/side-channel-list/index.js delete mode 100644 node_modules/side-channel-list/list.d.ts delete mode 100644 node_modules/side-channel-list/package.json delete mode 100644 node_modules/side-channel-list/test/index.js delete mode 100644 node_modules/side-channel-list/tsconfig.json delete mode 100644 node_modules/side-channel-map/.editorconfig delete mode 100644 node_modules/side-channel-map/.eslintrc delete mode 100644 node_modules/side-channel-map/.github/FUNDING.yml delete mode 100644 node_modules/side-channel-map/.nycrc delete mode 100644 node_modules/side-channel-map/CHANGELOG.md delete mode 100644 node_modules/side-channel-map/LICENSE delete mode 100644 node_modules/side-channel-map/README.md delete mode 100644 node_modules/side-channel-map/index.d.ts delete mode 100644 node_modules/side-channel-map/index.js delete mode 100644 node_modules/side-channel-map/package.json delete mode 100644 node_modules/side-channel-map/test/index.js delete mode 100644 node_modules/side-channel-map/tsconfig.json delete mode 100644 node_modules/side-channel-weakmap/.editorconfig delete mode 100644 node_modules/side-channel-weakmap/.eslintrc delete mode 100644 node_modules/side-channel-weakmap/.github/FUNDING.yml delete mode 100644 node_modules/side-channel-weakmap/.nycrc delete mode 100644 node_modules/side-channel-weakmap/CHANGELOG.md delete mode 100644 node_modules/side-channel-weakmap/LICENSE delete mode 100644 node_modules/side-channel-weakmap/README.md delete mode 100644 node_modules/side-channel-weakmap/index.d.ts delete mode 100644 node_modules/side-channel-weakmap/index.js delete mode 100644 node_modules/side-channel-weakmap/package.json delete mode 100644 node_modules/side-channel-weakmap/test/index.js delete mode 100644 node_modules/side-channel-weakmap/tsconfig.json delete mode 100644 node_modules/side-channel/.editorconfig delete mode 100644 node_modules/side-channel/.eslintrc delete mode 100644 node_modules/side-channel/.github/FUNDING.yml delete mode 100644 node_modules/side-channel/.nycrc delete mode 100644 node_modules/side-channel/CHANGELOG.md delete mode 100644 node_modules/side-channel/LICENSE delete mode 100644 node_modules/side-channel/README.md delete mode 100644 node_modules/side-channel/index.d.ts delete mode 100644 node_modules/side-channel/index.js delete mode 100644 node_modules/side-channel/package.json delete mode 100644 node_modules/side-channel/test/index.js delete mode 100644 node_modules/side-channel/tsconfig.json delete mode 100644 node_modules/simple-update-notifier/LICENSE delete mode 100644 node_modules/simple-update-notifier/README.md delete mode 100644 node_modules/simple-update-notifier/build/index.d.ts delete mode 100644 node_modules/simple-update-notifier/build/index.js delete mode 100644 node_modules/simple-update-notifier/package.json delete mode 100644 node_modules/simple-update-notifier/src/borderedText.ts delete mode 100644 node_modules/simple-update-notifier/src/cache.spec.ts delete mode 100644 node_modules/simple-update-notifier/src/cache.ts delete mode 100644 node_modules/simple-update-notifier/src/getDistVersion.spec.ts delete mode 100644 node_modules/simple-update-notifier/src/getDistVersion.ts delete mode 100644 node_modules/simple-update-notifier/src/hasNewVersion.spec.ts delete mode 100644 node_modules/simple-update-notifier/src/hasNewVersion.ts delete mode 100644 node_modules/simple-update-notifier/src/index.spec.ts delete mode 100644 node_modules/simple-update-notifier/src/index.ts delete mode 100644 node_modules/simple-update-notifier/src/isNpmOrYarn.ts delete mode 100644 node_modules/simple-update-notifier/src/types.ts delete mode 100644 node_modules/socket.io-adapter/LICENSE delete mode 100644 node_modules/socket.io-adapter/Readme.md delete mode 100644 node_modules/socket.io-adapter/dist/cluster-adapter.d.ts delete mode 100644 node_modules/socket.io-adapter/dist/cluster-adapter.js delete mode 100644 node_modules/socket.io-adapter/dist/contrib/yeast.d.ts delete mode 100644 node_modules/socket.io-adapter/dist/contrib/yeast.js delete mode 100644 node_modules/socket.io-adapter/dist/in-memory-adapter.d.ts delete mode 100644 node_modules/socket.io-adapter/dist/in-memory-adapter.js delete mode 100644 node_modules/socket.io-adapter/dist/index.d.ts delete mode 100644 node_modules/socket.io-adapter/dist/index.js delete mode 100644 node_modules/socket.io-adapter/node_modules/debug/LICENSE delete mode 100644 node_modules/socket.io-adapter/node_modules/debug/README.md delete mode 100644 node_modules/socket.io-adapter/node_modules/debug/package.json delete mode 100644 node_modules/socket.io-adapter/node_modules/debug/src/browser.js delete mode 100644 node_modules/socket.io-adapter/node_modules/debug/src/common.js delete mode 100644 node_modules/socket.io-adapter/node_modules/debug/src/index.js delete mode 100644 node_modules/socket.io-adapter/node_modules/debug/src/node.js delete mode 100644 node_modules/socket.io-adapter/node_modules/ms/index.js delete mode 100644 node_modules/socket.io-adapter/node_modules/ms/license.md delete mode 100644 node_modules/socket.io-adapter/node_modules/ms/package.json delete mode 100644 node_modules/socket.io-adapter/node_modules/ms/readme.md delete mode 100644 node_modules/socket.io-adapter/package.json delete mode 100644 node_modules/socket.io-parser/LICENSE delete mode 100644 node_modules/socket.io-parser/Readme.md delete mode 100644 node_modules/socket.io-parser/build/cjs/binary.d.ts delete mode 100644 node_modules/socket.io-parser/build/cjs/binary.js delete mode 100644 node_modules/socket.io-parser/build/cjs/index.d.ts delete mode 100644 node_modules/socket.io-parser/build/cjs/index.js delete mode 100644 node_modules/socket.io-parser/build/cjs/is-binary.d.ts delete mode 100644 node_modules/socket.io-parser/build/cjs/is-binary.js delete mode 100644 node_modules/socket.io-parser/build/cjs/package.json delete mode 100644 node_modules/socket.io-parser/build/esm-debug/binary.d.ts delete mode 100644 node_modules/socket.io-parser/build/esm-debug/binary.js delete mode 100644 node_modules/socket.io-parser/build/esm-debug/index.d.ts delete mode 100644 node_modules/socket.io-parser/build/esm-debug/index.js delete mode 100644 node_modules/socket.io-parser/build/esm-debug/is-binary.d.ts delete mode 100644 node_modules/socket.io-parser/build/esm-debug/is-binary.js delete mode 100644 node_modules/socket.io-parser/build/esm-debug/package.json delete mode 100644 node_modules/socket.io-parser/build/esm/binary.d.ts delete mode 100644 node_modules/socket.io-parser/build/esm/binary.js delete mode 100644 node_modules/socket.io-parser/build/esm/index.d.ts delete mode 100644 node_modules/socket.io-parser/build/esm/index.js delete mode 100644 node_modules/socket.io-parser/build/esm/is-binary.d.ts delete mode 100644 node_modules/socket.io-parser/build/esm/is-binary.js delete mode 100644 node_modules/socket.io-parser/build/esm/package.json delete mode 100644 node_modules/socket.io-parser/node_modules/debug/LICENSE delete mode 100644 node_modules/socket.io-parser/node_modules/debug/README.md delete mode 100644 node_modules/socket.io-parser/node_modules/debug/package.json delete mode 100644 node_modules/socket.io-parser/node_modules/debug/src/browser.js delete mode 100644 node_modules/socket.io-parser/node_modules/debug/src/common.js delete mode 100644 node_modules/socket.io-parser/node_modules/debug/src/index.js delete mode 100644 node_modules/socket.io-parser/node_modules/debug/src/node.js delete mode 100644 node_modules/socket.io-parser/node_modules/ms/index.js delete mode 100644 node_modules/socket.io-parser/node_modules/ms/license.md delete mode 100644 node_modules/socket.io-parser/node_modules/ms/package.json delete mode 100644 node_modules/socket.io-parser/node_modules/ms/readme.md delete mode 100644 node_modules/socket.io-parser/package.json delete mode 100644 node_modules/socket.io/LICENSE delete mode 100644 node_modules/socket.io/Readme.md delete mode 100644 node_modules/socket.io/client-dist/socket.io.esm.min.js delete mode 100644 node_modules/socket.io/client-dist/socket.io.esm.min.js.map delete mode 100644 node_modules/socket.io/client-dist/socket.io.js delete mode 100644 node_modules/socket.io/client-dist/socket.io.js.map delete mode 100644 node_modules/socket.io/client-dist/socket.io.min.js delete mode 100644 node_modules/socket.io/client-dist/socket.io.min.js.map delete mode 100644 node_modules/socket.io/client-dist/socket.io.msgpack.min.js delete mode 100644 node_modules/socket.io/client-dist/socket.io.msgpack.min.js.map delete mode 100644 node_modules/socket.io/dist/broadcast-operator.d.ts delete mode 100644 node_modules/socket.io/dist/broadcast-operator.js delete mode 100644 node_modules/socket.io/dist/client.d.ts delete mode 100644 node_modules/socket.io/dist/client.js delete mode 100644 node_modules/socket.io/dist/index.d.ts delete mode 100644 node_modules/socket.io/dist/index.js delete mode 100644 node_modules/socket.io/dist/namespace.d.ts delete mode 100644 node_modules/socket.io/dist/namespace.js delete mode 100644 node_modules/socket.io/dist/parent-namespace.d.ts delete mode 100644 node_modules/socket.io/dist/parent-namespace.js delete mode 100644 node_modules/socket.io/dist/socket-types.d.ts delete mode 100644 node_modules/socket.io/dist/socket-types.js delete mode 100644 node_modules/socket.io/dist/socket.d.ts delete mode 100644 node_modules/socket.io/dist/socket.js delete mode 100644 node_modules/socket.io/dist/typed-events.d.ts delete mode 100644 node_modules/socket.io/dist/typed-events.js delete mode 100644 node_modules/socket.io/dist/uws.d.ts delete mode 100644 node_modules/socket.io/dist/uws.js delete mode 100644 node_modules/socket.io/node_modules/debug/LICENSE delete mode 100644 node_modules/socket.io/node_modules/debug/README.md delete mode 100644 node_modules/socket.io/node_modules/debug/package.json delete mode 100644 node_modules/socket.io/node_modules/debug/src/browser.js delete mode 100644 node_modules/socket.io/node_modules/debug/src/common.js delete mode 100644 node_modules/socket.io/node_modules/debug/src/index.js delete mode 100644 node_modules/socket.io/node_modules/debug/src/node.js delete mode 100644 node_modules/socket.io/node_modules/ms/index.js delete mode 100644 node_modules/socket.io/node_modules/ms/license.md delete mode 100644 node_modules/socket.io/node_modules/ms/package.json delete mode 100644 node_modules/socket.io/node_modules/ms/readme.md delete mode 100644 node_modules/socket.io/package.json delete mode 100644 node_modules/socket.io/wrapper.mjs delete mode 100644 node_modules/statuses/HISTORY.md delete mode 100644 node_modules/statuses/LICENSE delete mode 100644 node_modules/statuses/README.md delete mode 100644 node_modules/statuses/codes.json delete mode 100644 node_modules/statuses/index.js delete mode 100644 node_modules/statuses/package.json delete mode 100644 node_modules/supports-color/browser.js delete mode 100644 node_modules/supports-color/index.js delete mode 100644 node_modules/supports-color/license delete mode 100644 node_modules/supports-color/package.json delete mode 100644 node_modules/supports-color/readme.md delete mode 100644 node_modules/to-regex-range/LICENSE delete mode 100644 node_modules/to-regex-range/README.md delete mode 100644 node_modules/to-regex-range/index.js delete mode 100644 node_modules/to-regex-range/package.json delete mode 100644 node_modules/toidentifier/HISTORY.md delete mode 100644 node_modules/toidentifier/LICENSE delete mode 100644 node_modules/toidentifier/README.md delete mode 100644 node_modules/toidentifier/index.js delete mode 100644 node_modules/toidentifier/package.json delete mode 100644 node_modules/touch/LICENSE delete mode 100644 node_modules/touch/README.md delete mode 100755 node_modules/touch/bin/nodetouch.js delete mode 100644 node_modules/touch/index.js delete mode 100644 node_modules/touch/package.json delete mode 100644 node_modules/type-is/HISTORY.md delete mode 100644 node_modules/type-is/LICENSE delete mode 100644 node_modules/type-is/README.md delete mode 100644 node_modules/type-is/index.js delete mode 100644 node_modules/type-is/package.json delete mode 100644 node_modules/undefsafe/.github/workflows/release.yml delete mode 100644 node_modules/undefsafe/.jscsrc delete mode 100644 node_modules/undefsafe/.jshintrc delete mode 100644 node_modules/undefsafe/.travis.yml delete mode 100644 node_modules/undefsafe/LICENSE delete mode 100644 node_modules/undefsafe/README.md delete mode 100644 node_modules/undefsafe/example.js delete mode 100644 node_modules/undefsafe/lib/undefsafe.js delete mode 100644 node_modules/undefsafe/package.json delete mode 100644 node_modules/undici-types/LICENSE delete mode 100644 node_modules/undici-types/README.md delete mode 100644 node_modules/undici-types/agent.d.ts delete mode 100644 node_modules/undici-types/api.d.ts delete mode 100644 node_modules/undici-types/balanced-pool.d.ts delete mode 100644 node_modules/undici-types/cache-interceptor.d.ts delete mode 100644 node_modules/undici-types/cache.d.ts delete mode 100644 node_modules/undici-types/client-stats.d.ts delete mode 100644 node_modules/undici-types/client.d.ts delete mode 100644 node_modules/undici-types/connector.d.ts delete mode 100644 node_modules/undici-types/content-type.d.ts delete mode 100644 node_modules/undici-types/cookies.d.ts delete mode 100644 node_modules/undici-types/diagnostics-channel.d.ts delete mode 100644 node_modules/undici-types/dispatcher.d.ts delete mode 100644 node_modules/undici-types/env-http-proxy-agent.d.ts delete mode 100644 node_modules/undici-types/errors.d.ts delete mode 100644 node_modules/undici-types/eventsource.d.ts delete mode 100644 node_modules/undici-types/fetch.d.ts delete mode 100644 node_modules/undici-types/formdata.d.ts delete mode 100644 node_modules/undici-types/global-dispatcher.d.ts delete mode 100644 node_modules/undici-types/global-origin.d.ts delete mode 100644 node_modules/undici-types/h2c-client.d.ts delete mode 100644 node_modules/undici-types/handlers.d.ts delete mode 100644 node_modules/undici-types/header.d.ts delete mode 100644 node_modules/undici-types/index.d.ts delete mode 100644 node_modules/undici-types/interceptors.d.ts delete mode 100644 node_modules/undici-types/mock-agent.d.ts delete mode 100644 node_modules/undici-types/mock-call-history.d.ts delete mode 100644 node_modules/undici-types/mock-client.d.ts delete mode 100644 node_modules/undici-types/mock-errors.d.ts delete mode 100644 node_modules/undici-types/mock-interceptor.d.ts delete mode 100644 node_modules/undici-types/mock-pool.d.ts delete mode 100644 node_modules/undici-types/package.json delete mode 100644 node_modules/undici-types/patch.d.ts delete mode 100644 node_modules/undici-types/pool-stats.d.ts delete mode 100644 node_modules/undici-types/pool.d.ts delete mode 100644 node_modules/undici-types/proxy-agent.d.ts delete mode 100644 node_modules/undici-types/readable.d.ts delete mode 100644 node_modules/undici-types/retry-agent.d.ts delete mode 100644 node_modules/undici-types/retry-handler.d.ts delete mode 100644 node_modules/undici-types/util.d.ts delete mode 100644 node_modules/undici-types/utility.d.ts delete mode 100644 node_modules/undici-types/webidl.d.ts delete mode 100644 node_modules/undici-types/websocket.d.ts delete mode 100644 node_modules/unpipe/HISTORY.md delete mode 100644 node_modules/unpipe/LICENSE delete mode 100644 node_modules/unpipe/README.md delete mode 100644 node_modules/unpipe/index.js delete mode 100644 node_modules/unpipe/package.json delete mode 100644 node_modules/utils-merge/.npmignore delete mode 100644 node_modules/utils-merge/LICENSE delete mode 100644 node_modules/utils-merge/README.md delete mode 100644 node_modules/utils-merge/index.js delete mode 100644 node_modules/utils-merge/package.json delete mode 100644 node_modules/vary/HISTORY.md delete mode 100644 node_modules/vary/LICENSE delete mode 100644 node_modules/vary/README.md delete mode 100644 node_modules/vary/index.js delete mode 100644 node_modules/vary/package.json delete mode 100644 node_modules/ws/LICENSE delete mode 100644 node_modules/ws/README.md delete mode 100644 node_modules/ws/browser.js delete mode 100644 node_modules/ws/index.js delete mode 100644 node_modules/ws/lib/buffer-util.js delete mode 100644 node_modules/ws/lib/constants.js delete mode 100644 node_modules/ws/lib/event-target.js delete mode 100644 node_modules/ws/lib/extension.js delete mode 100644 node_modules/ws/lib/limiter.js delete mode 100644 node_modules/ws/lib/permessage-deflate.js delete mode 100644 node_modules/ws/lib/receiver.js delete mode 100644 node_modules/ws/lib/sender.js delete mode 100644 node_modules/ws/lib/stream.js delete mode 100644 node_modules/ws/lib/subprotocol.js delete mode 100644 node_modules/ws/lib/validation.js delete mode 100644 node_modules/ws/lib/websocket-server.js delete mode 100644 node_modules/ws/lib/websocket.js delete mode 100644 node_modules/ws/package.json delete mode 100644 node_modules/ws/wrapper.mjs delete mode 100644 package-lock.json delete mode 100644 package.json create mode 100644 server_test.py delete mode 100644 test-server.js diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000..7ef04e2 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..871e2b2 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..f0c6ad0 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,50 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.kotlin/errors/errors-1759321347725.log b/.kotlin/errors/errors-1759321347725.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1759321347725.log @@ -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 + diff --git a/.kotlin/errors/errors-1759400006923.log b/.kotlin/errors/errors-1759400006923.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1759400006923.log @@ -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 + diff --git a/.kotlin/errors/errors-1759466115565.log b/.kotlin/errors/errors-1759466115565.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1759466115565.log @@ -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 + diff --git a/.kotlin/errors/errors-1759530749084.log b/.kotlin/errors/errors-1759530749084.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1759530749084.log @@ -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 + diff --git a/app/build-legacy.gradle.kts b/app/build-legacy.gradle.kts new file mode 100644 index 0000000..5fa213c --- /dev/null +++ b/app/build-legacy.gradle.kts @@ -0,0 +1,73 @@ +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" + } + + 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") +} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b8453d7..184736c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,19 +1,18 @@ plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) + id("com.android.application") + id("org.jetbrains.kotlin.android") } android { namespace = "com.example.godeye" - compileSdk = 36 + compileSdk = 29 // Понижаем до Android 10 defaultConfig { applicationId = "com.example.godeye" minSdk = 24 - targetSdk = 36 + targetSdk = 28 // Понижаем до Android 9 versionCode = 1 - versionName = "1.0" + versionName = "1.0-legacy" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -27,79 +26,62 @@ android { ) } } + compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = "17" - } - buildFeatures { - compose = true - viewBinding = true + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } - // Исправляем проблему с Java toolchain - java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } + kotlinOptions { + jvmTarget = "1.8" + } + + // ОТКЛЮЧАЕМ COMPOSE ДЛЯ LEGACY ВЕРСИИ + buildFeatures { + compose = false + viewBinding = true } } dependencies { - // Core Android - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.compose.material3) + // ЭКСТРЕМАЛЬНО СТАРЫЕ зависимости для 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 - // ViewModel and LiveData - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") - implementation("androidx.activity:activity-ktx:1.8.2") + // Классический 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 - // Socket.IO для WebSocket соединения - implementation("io.socket:socket.io-client:2.1.2") + // СТАРЫЕ 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 - // Пока уберем WebRTC зависимость - создадим заглушку для демонстрации - // В реальном проекте нужно будет настроить правильную WebRTC библиотеку + // УБИРАЕМ СОВРЕМЕННЫЕ CAMERA БИБЛИОТЕКИ + // Вместо CameraX используем старую Camera2 API напрямую - // Camera2 API - implementation("androidx.camera:camera-core:1.3.1") - implementation("androidx.camera:camera-camera2:1.3.1") - implementation("androidx.camera:camera-lifecycle:1.3.1") - implementation("androidx.camera:camera-view:1.3.1") + // Socket.IO и базовые сетевые библиотеки + implementation("io.socket:socket.io-client:2.1.0") + implementation("com.google.code.gson:gson:2.8.9") // Старая версия - // JSON парсинг - implementation("com.google.code.gson:gson:2.10.1") + // УБИРАЕМ WebRTC полностью для стабильности + // implementation("io.getstream:stream-webrtc-android:1.0.4") - // Корутины - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + // Старые корутины + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") // 2021 год - // RecyclerView - implementation("androidx.recyclerview:recyclerview:1.3.2") + // Базовые зависимости - старые версии + implementation("androidx.recyclerview:recyclerview:1.2.1") // 2021 год - // 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") + // УБИРАЕМ Work Manager и Activity KTX + // implementation("androidx.work:work-runtime-ktx:2.8.1") + // implementation("androidx.activity:activity-ktx:1.7.2") // Testing - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.compose.ui.test.junit4) - debugImplementation(libs.androidx.compose.ui.tooling) - debugImplementation(libs.androidx.compose.ui.test.manifest) -} \ No newline at end of file + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.3") // Старая версия + androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") // Старая версия +} diff --git a/app/src/main/AndroidManifest-legacy.xml b/app/src/main/AndroidManifest-legacy.xml new file mode 100644 index 0000000..3881c7e --- /dev/null +++ b/app/src/main/AndroidManifest-legacy.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5e388e4..6933832 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,56 +2,67 @@ - + - - - + + android:theme="@style/Theme.GodEye" + android:networkSecurityConfig="@xml/network_security_config" + android:usesCleartextTraffic="true" + tools:targetApi="31"> + + android:screenOrientation="portrait" + android:launchMode="singleTop"> - + + + + + + + + android:exported="false" /> - - - \ No newline at end of file + diff --git a/app/src/main/java/com/example/godeye/GodEyeApplication.kt b/app/src/main/java/com/example/godeye/GodEyeApplication.kt new file mode 100644 index 0000000..d661b90 --- /dev/null +++ b/app/src/main/java/com/example/godeye/GodEyeApplication.kt @@ -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() + } +} diff --git a/app/src/main/java/com/example/godeye/LegacyCameraActivity.kt b/app/src/main/java/com/example/godeye/LegacyCameraActivity.kt new file mode 100644 index 0000000..065ca7a --- /dev/null +++ b/app/src/main/java/com/example/godeye/LegacyCameraActivity.kt @@ -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, 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) + } + } +} diff --git a/app/src/main/java/com/example/godeye/LegacyMainActivity.kt b/app/src/main/java/com/example/godeye/LegacyMainActivity.kt new file mode 100644 index 0000000..1ac50f6 --- /dev/null +++ b/app/src/main/java/com/example/godeye/LegacyMainActivity.kt @@ -0,0 +1,240 @@ +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.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) + startForegroundService(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) + } + } +} diff --git a/app/src/main/java/com/example/godeye/MainActivity.kt b/app/src/main/java/com/example/godeye/MainActivity.kt index fce2244..528e40c 100644 --- a/app/src/main/java/com/example/godeye/MainActivity.kt +++ b/app/src/main/java/com/example/godeye/MainActivity.kt @@ -1,118 +1,431 @@ package com.example.godeye -import android.Manifest -import android.content.pm.PackageManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.os.Bundle +import android.os.IBinder import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +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.core.content.ContextCompat +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.godeye.camera.CameraScreen import com.example.godeye.managers.PermissionManager -import com.example.godeye.ui.screens.MainScreen +import com.example.godeye.models.* +import com.example.godeye.services.SocketService +import com.example.godeye.ui.components.* +import com.example.godeye.ui.theme.GodEyeColors import com.example.godeye.ui.theme.GodEyeTheme -import com.example.godeye.ui.viewmodels.MainViewModel +import com.example.godeye.utils.ErrorHandler import com.example.godeye.utils.Logger import kotlinx.coroutines.launch +/** + * MainActivity - упрощенная версия для Android 9 + * БЕЗ сложных анимаций и градиентов + */ +@OptIn(ExperimentalMaterial3Api::class) class MainActivity : ComponentActivity() { private val viewModel: MainViewModel by viewModels() - private lateinit var permissionManager: PermissionManager + private val errorHandler = ErrorHandler() + private var socketService: SocketService? = null - // Launcher для запроса разрешений - private val permissionsLauncher = registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { 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(", ")}") + // Подключение к SocketService + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Logger.step("SERVICE_CONNECTED", "SocketService connected to MainActivity") + val binder = service as SocketService.LocalBinder + socketService = binder.getService() + viewModel.bindToSocketService(socketService!!) } - // Логируем статус разрешений - permissionManager.logPermissionsStatus() + override fun onServiceDisconnected(name: ComponentName?) { + Logger.step("SERVICE_DISCONNECTED", "SocketService disconnected from MainActivity") + socketService = null + } + } + + // Обработка разрешений + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + Logger.step("PERMISSIONS_RESULT", "Permission request result received") + val allGranted = permissions.values.all { it } + if (allGranted) { + Logger.step("PERMISSIONS_ALL_GRANTED", "All permissions granted") + viewModel.onPermissionsGranted() + } else { + val denied = permissions.filterValues { !it }.keys + Logger.step("PERMISSIONS_DENIED", "Some permissions denied: ${denied.joinToString()}") + val permissionManager = PermissionManager(this) + val hasCritical = denied.any { it in PermissionManager.CRITICAL_PERMISSIONS } + if (hasCritical) { + errorHandler.handleError(AppError.CameraPermissionDenied, this) + } + } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Logger.d("MainActivity created") - permissionManager = PermissionManager(this) + try { + Logger.step("ACTIVITY_CREATE", "MainActivity onCreate simplified for Android 9") + Logger.d("Device: ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}") + Logger.d("Android: ${android.os.Build.VERSION.RELEASE}") - // Проверяем разрешения при запуске - checkAndRequestPermissions() - if (permissionManager.hasAllRequiredPermissions()) { - viewModel.startServices() // Запуск сервисов если разрешения уже есть - } + // Запуск SocketService + startAndBindSocketService() - setContent { - GodEyeTheme { - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() + setContent { + GodEyeTheme { + var showSettings by remember { mutableStateOf(false) } + var showCamera by remember { mutableStateOf(false) } + val cameraRequest by viewModel.cameraRequest.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Scaffold( - snackbarHost = { SnackbarHost(snackbarHostState) } - ) { paddingValues -> - MainScreen( - viewModel = viewModel, - onRequestPermissions = { - requestMissingPermissions() - }, - onShowError = { message -> - coroutineScope.launch { - snackbarHostState.showSnackbar(message) - } + // Автоматическое принятие запросов камеры + LaunchedEffect(cameraRequest) { + val currentRequest = cameraRequest + if (currentRequest != null) { + Logger.step("AUTO_ACCEPT_CAMERA_REQUEST", "Auto-accepting camera request") + showCamera = true + viewModel.acceptCameraRequest(currentRequest.sessionId, "Auto-accepted") + } else { + showCamera = false + } + } + + // Обработка ошибок + val connectionState by viewModel.connectionState.collectAsState() + LaunchedEffect(connectionState) { + if (connectionState == ConnectionState.ERROR) { + errorHandler.handleError(AppError.NetworkError, this@MainActivity, scope, snackbarHostState) + } + } + + Box(modifier = Modifier.fillMaxSize()) { + when { + showCamera && cameraRequest != null -> { + Logger.step("UI_RENDERING_CAMERA", "Rendering simplified CameraScreen") + CameraScreen( + onBackPressed = { + Logger.step("CAMERA_BACK_PRESSED", "User pressed back") + showCamera = false + viewModel.clearCameraRequest() + }, + sessionId = cameraRequest!!.sessionId, + operatorId = cameraRequest!!.operatorId + ) } + showSettings -> { + Logger.step("UI_RENDERING_SETTINGS", "Rendering SettingsScreen") + SettingsScreen( + onBackPressed = { showSettings = false }, + onServerConfigSaved = { url -> + viewModel.updateServerUrl(url) + showSettings = false + viewModel.connectToServer() + } + ) + } + else -> { + Logger.step("UI_RENDERING_MAIN", "Rendering simplified MainScreen") + SimplifiedMainScreen( + onSettingsClick = { showSettings = true }, + onCameraAccept = { showCamera = true }, + snackbarHostState = snackbarHostState + ) + } + } + + // Простой Snackbar для ошибок + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier.align(Alignment.BottomCenter) ) } } } + + // Проверка разрешений + checkRequiredPermissions() + Logger.step("ACTIVITY_CREATE_COMPLETE", "MainActivity simplified onCreate complete") + + } catch (e: Exception) { + Logger.error("ACTIVITY_CREATE_ERROR", "Error in MainActivity onCreate", e) + errorHandler.handleError(AppError.UnknownError(e), this) + } + } + + private fun startAndBindSocketService() { + Logger.step("SOCKET_SERVICE_START", "Starting SocketService") + val intent = Intent(this, SocketService::class.java) + startForegroundService(intent) + bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + } + + private fun checkRequiredPermissions() { + Logger.step("PERMISSION_CHECK", "Checking permissions") + val permissionManager = PermissionManager(this) + if (!permissionManager.checkPermissions()) { + val missingPermissions = permissionManager.getMissingPermissions() + Logger.step("PERMISSIONS_MISSING", "Requesting: ${missingPermissions.joinToString()}") + permissionLauncher.launch(missingPermissions) + } else { + Logger.step("PERMISSIONS_OK", "All permissions granted") + viewModel.onPermissionsGranted() + } + } + + @Composable + private fun SimplifiedMainScreen( + onSettingsClick: () -> Unit, + onCameraAccept: () -> Unit, + snackbarHostState: SnackbarHostState + ) { + val connectionState by viewModel.connectionState.collectAsState() + val serverUrl by viewModel.serverUrl.collectAsState() + val deviceId by viewModel.deviceId.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + val cameraRequest by viewModel.cameraRequest.collectAsState() + val isStreaming by viewModel.isStreaming.collectAsState() + val activeSessions by viewModel.activeSessions.collectAsState() + val permissionsGranted by viewModel.permissionsGranted.collectAsState() + val scope = rememberCoroutineScope() + + // УПРОЩЕННЫЙ UI ДЛЯ ANDROID 9 - БЕЗ СЛОЖНЫХ АНИМАЦИЙ + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Простой заголовок + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color.Gray.copy(alpha = 0.3f)) + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "GodEye Signal Center", + style = MaterialTheme.typography.headlineMedium, + color = Color.White + ) + Text( + text = "Android Client v1.0 (Simplified)", + style = MaterialTheme.typography.bodyMedium, + color = Color.Gray + ) + } + } + + // Разрешения + if (!permissionsGranted) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color.Red.copy(alpha = 0.7f)) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("⚠️ Требуются разрешения", color = Color.White) + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { + val permissionManager = PermissionManager(this@MainActivity) + permissionLauncher.launch(permissionManager.getMissingPermissions()) + } + ) { + Text("Предоставить разрешения") + } + } + } + } + + // Device ID + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color.Blue.copy(alpha = 0.3f)) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text("📱 Device ID", color = Color.White) + Text(deviceId.take(16) + "...", color = Color.Gray) + Text("${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}", color = Color.Gray) + } + } + + // Подключение к серверу + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = when (connectionState) { + ConnectionState.CONNECTED -> Color.Green.copy(alpha = 0.3f) + ConnectionState.ERROR -> Color.Red.copy(alpha = 0.3f) + else -> Color.Yellow.copy(alpha = 0.3f) + } + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text("🌐 Сервер", color = Color.White) + Text( + when (connectionState) { + ConnectionState.CONNECTED -> "✅ Подключено" + ConnectionState.CONNECTING -> "🔄 Подключение..." + ConnectionState.ERROR -> "❌ Ошибка" + else -> "⚪ Отключено" + }, + color = Color.White + ) + Text("$serverUrl", color = Color.Gray) + + Spacer(modifier = Modifier.height(8.dp)) + + if (connectionState == ConnectionState.DISCONNECTED) { + Button( + onClick = { + if (permissionsGranted) { + viewModel.connectToServer() + } else { + scope.launch { + snackbarHostState.showSnackbar("Нужны разрешения") + } + } + }, + enabled = !isLoading, + modifier = Modifier.fillMaxWidth() + ) { + Text(if (isLoading) "Подключение..." else "🔗 Подключиться") + } + } else { + Button( + onClick = viewModel::disconnect, + modifier = Modifier.fillMaxWidth() + ) { + Text("❌ Отключиться") + } + } + } + } + + // Статус трансляции + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (isStreaming) Color.Green.copy(alpha = 0.3f) else Color.Gray.copy(alpha = 0.3f) + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text("📹 Трансляция", color = Color.White) + Text( + if (isStreaming) "🔴 Активна: ${activeSessions.size} сессий" else "⚪ Неактивна", + color = Color.White + ) + } + } + + // Кнопки управления + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = onCameraAccept, + enabled = permissionsGranted, + modifier = Modifier.weight(1f) + ) { + Text("📷 Камера") + } + Button( + onClick = onSettingsClick, + modifier = Modifier.weight(1f) + ) { + Text("⚙️ Настройки") + } + } + + // Кнопка для запуска Legacy версии + Button( + onClick = { + Logger.step("LAUNCH_LEGACY_VERSION", "Launching LegacyMainActivity") + val intent = Intent(this@MainActivity, LegacyMainActivity::class.java) + startActivity(intent) + }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF9C27B0) // Фиолетовый цвет для выделения + ) + ) { + Text("📱 Legacy версия (Android 9)") + } + + // Запрос от оператора + cameraRequest?.let { request -> + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color.Yellow.copy(alpha = 0.8f)) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("📞 Запрос от оператора", color = Color.Black) + Text("Сессия: ${request.sessionId.take(8)}...", color = Color.Black) + + Spacer(modifier = Modifier.height(8.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button( + onClick = { + viewModel.acceptCameraRequest(request.sessionId, "Принято") + onCameraAccept() + }, + modifier = Modifier.weight(1f) + ) { + Text("✅ Принять") + } + Button( + onClick = { + viewModel.rejectCameraRequest(request.sessionId, "Отклонено") + }, + modifier = Modifier.weight(1f) + ) { + Text("❌ Отклонить") + } + } + } + } + } } } - /** - * Проверить и запросить недостающие разрешения - */ - private fun checkAndRequestPermissions() { - if (!permissionManager.hasAllRequiredPermissions()) { - Logger.d("Some permissions are missing, requesting...") - requestMissingPermissions() - } else { - Logger.d("All required permissions are granted") - } - } - - /** - * Запросить недостающие разрешения - */ - private fun requestMissingPermissions() { - val missingPermissions = permissionManager.getMissingPermissions() - if (missingPermissions.isNotEmpty()) { - Logger.d("Requesting permissions: ${missingPermissions.joinToString(", ")}") - permissionsLauncher.launch(missingPermissions.toTypedArray()) - } - } - override fun onDestroy() { + Logger.step("ACTIVITY_DESTROY", "MainActivity destroyed") + try { + unbindService(serviceConnection) + } catch (e: Exception) { + Logger.error("UNBIND_SERVICE_ERROR", "Error unbinding SocketService", e) + } super.onDestroy() - Logger.d("MainActivity destroyed") } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/godeye/MainViewModel.kt b/app/src/main/java/com/example/godeye/MainViewModel.kt new file mode 100644 index 0000000..014a56b --- /dev/null +++ b/app/src/main/java/com/example/godeye/MainViewModel.kt @@ -0,0 +1,500 @@ +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 com.example.godeye.webrtc.WebRTCManager +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() + + // Управляющие компоненты согласно ТЗ + 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.asStateFlow() + + private val _serverUrl = MutableStateFlow("") + val serverUrl: StateFlow = _serverUrl.asStateFlow() + + private val _deviceId = MutableStateFlow("") + val deviceId: StateFlow = _deviceId.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + // Управление сессиями согласно ТЗ + private val _cameraRequest = MutableStateFlow(null) + val cameraRequest: StateFlow = _cameraRequest.asStateFlow() + + private val _activeSessions = MutableStateFlow>(emptyMap()) + val activeSessions: StateFlow> = _activeSessions.asStateFlow() + + private val _isStreaming = MutableStateFlow(false) + val isStreaming: StateFlow = _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 + + viewModelScope.launch { + // Наблюдение за состоянием подключения + service.connectionState.collect { state -> + _connectionState.value = state + Logger.step("CONNECTION_STATE_CHANGED", "Connection state: $state") + } + } + + viewModelScope.launch { + // Наблюдение за запросами камеры от операторов + service.cameraRequests.collect { request -> + if (request != null) { + Logger.step("CAMERA_REQUEST_RECEIVED", + "Camera request from ${request.operatorId} for ${request.cameraType}") + _cameraRequest.value = request + } + } + } + + viewModelScope.launch { + // Наблюдение за WebRTC событиями + service.webRTCEvents.collect { event -> + event?.let { handleWebRTCEvent(it) } + } + } + + viewModelScope.launch { + // Наблюдение за сессиями + sessionManager.sessions.collect { sessions -> + val sessionInfo = sessions.mapValues { (sessionId, session) -> + SessionInfo( + sessionId = sessionId, + deviceId = _deviceId.value, + operatorId = session.operatorId, + cameraType = session.cameraType, + status = if (session.webRTCConnected) "Connected" else "Connecting", + createdAt = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()) + .format(java.util.Date(session.startTime)) + ) + } + _activeSessions.value = sessionInfo + _isStreaming.value = sessions.values.any { it.isActive } + } + } + } + + /** + * Инициализация WebRTC согласно ТЗ + */ + private fun initializeWebRTC() { + webRTCManager = WebRTCManager(context) { message -> + // Обработка сигнальных сообщений через SocketService + Logger.step("WEBRTC_SIGNALING", "WebRTC signaling message: ${message.getString("type")}") + } + } + + /** + * Подключение к серверу согласно ТЗ (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(sessionId, 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.Offer -> { + Logger.step("WEBRTC_OFFER", "Processing WebRTC offer for session: ${event.sessionId}") + webRTCManager?.handleOffer(event.sessionId, event.offer) + } + is com.example.godeye.services.WebRTCEvent.Answer -> { + Logger.step("WEBRTC_ANSWER", "Processing WebRTC answer for session: ${event.sessionId}") + webRTCManager?.handleAnswer(event.sessionId, event.answer) + } + is com.example.godeye.services.WebRTCEvent.IceCandidate -> { + Logger.step("WEBRTC_ICE", "Processing ICE candidate for session: ${event.sessionId}") + webRTCManager?.handleIceCandidate(event.sessionId, event.candidate, event.sdpMid, event.sdpMLineIndex) + } + is com.example.godeye.services.WebRTCEvent.SwitchCamera -> { + Logger.step("WEBRTC_SWITCH_CAMERA", "Switching camera to: ${event.cameraType}") + switchCamera(event.cameraType) + } + } + } + + /** + * Остановка всех стримов + */ + 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(cameraType) + + // Обновляем тип камеры в активных сессиях + 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(sessionId) + + 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() + } + + /** + * Связывание с SocketService + */ + fun bindToSocketService(service: SocketService) { + Logger.step("VIEWMODEL_BIND_SERVICE", "Binding ViewModel to SocketService") + + viewModelScope.launch { + // Наблюдение за состоянием подключения + service.connectionState.collect { state -> + _connectionState.value = state + } + } + + viewModelScope.launch { + // Наблюдение за запросами камеры + service.cameraRequests.collect { request -> + _cameraRequest.value = request + } + } + + viewModelScope.launch { + // Наблюдение за WebRTC событиями + service.webRTCEvents.collect { event -> + event?.let { handleWebRTCEvent(it) } + } + } + } + + /** + * Callback при получении разрешений + */ + 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) { message -> + // Обработка сигналинга для тестового режима + Logger.d("Test signaling message: $message") + } + } + + // Запускаем стриминг + webRTCManager?.startStreaming(testSessionId, "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() + } +} diff --git a/app/src/main/java/com/example/godeye/SettingsScreen.kt b/app/src/main/java/com/example/godeye/SettingsScreen.kt new file mode 100644 index 0000000..a3ce94e --- /dev/null +++ b/app/src/main/java/com/example/godeye/SettingsScreen.kt @@ -0,0 +1,364 @@ +package com.example.godeye + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.core.content.edit +import com.example.godeye.ui.theme.GodEyeColors +import com.example.godeye.utils.getPreferences + +/** + * Экран настроек GodEye с расширенными параметрами согласно ТЗ + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + onBackPressed: () -> Unit, + onServerConfigSaved: (String) -> Unit +) { + val context = LocalContext.current + val prefs = context.getPreferences() + + // Состояния настроек + var serverUrl by remember { + mutableStateOf(prefs.getString("server_url", "http://192.168.219.108:3001") ?: "") + } + var deviceName by remember { + mutableStateOf(prefs.getString("device_name", "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}") ?: "") + } + var autoConnect by remember { + mutableStateOf(prefs.getBoolean("auto_connect", false)) + } + var autoAcceptRequests by remember { + mutableStateOf(prefs.getBoolean("auto_accept_requests", true)) + } + var enableNotifications by remember { + mutableStateOf(prefs.getBoolean("enable_notifications", true)) + } + var keepScreenOn by remember { + mutableStateOf(prefs.getBoolean("keep_screen_on", false)) + } + var preferredCamera by remember { + mutableStateOf(prefs.getString("preferred_camera", "back") ?: "back") + } + var streamQuality by remember { + mutableStateOf(prefs.getString("stream_quality", "720p") ?: "720p") + } + + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + // Шапка экрана + TopAppBar( + title = { + Text( + text = "Настройки GodEye", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = GodEyeColors.IvoryPure + ) + }, + navigationIcon = { + IconButton(onClick = onBackPressed) { + Icon( + Icons.Default.ArrowBack, + contentDescription = "Назад", + tint = GodEyeColors.IvoryPure + ) + } + }, + actions = { + TextButton( + onClick = { + // Сохраняем все настройки + prefs.edit { + putString("server_url", serverUrl) + putString("device_name", deviceName) + putBoolean("auto_connect", autoConnect) + putBoolean("auto_accept_requests", autoAcceptRequests) + putBoolean("enable_notifications", enableNotifications) + putBoolean("keep_screen_on", keepScreenOn) + putString("preferred_camera", preferredCamera) + putString("stream_quality", streamQuality) + } + onServerConfigSaved(serverUrl) + } + ) { + Text( + "Сохранить", + color = GodEyeColors.SuccessGreen, + fontWeight = FontWeight.Medium + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = GodEyeColors.BlackSoft.copy(alpha = 0.9f) + ) + ) + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Секция "Сервер" + item { + SettingsSection(title = "Подключение к серверу") { + OutlinedTextField( + value = serverUrl, + onValueChange = { serverUrl = it }, + label = { Text("URL сервера") }, + placeholder = { Text("http://192.168.1.100:3001") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = GodEyeColors.NavyLight, + unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f), + focusedTextColor = GodEyeColors.IvoryPure, + unfocusedTextColor = GodEyeColors.IvorySoft, + focusedLabelColor = GodEyeColors.NavyLight, + unfocusedLabelColor = GodEyeColors.IvorySoft + ), + leadingIcon = { + Icon( + Icons.Default.Language, + contentDescription = null, + tint = GodEyeColors.NavyLight + ) + } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsSwitchCard( + title = "Автоматическое подключение", + subtitle = "Подключаться к серверу при запуске приложения", + checked = autoConnect, + onCheckedChange = { autoConnect = it }, + icon = Icons.Default.AutoAwesome + ) + } + } + + // Секция "Устройство" + item { + SettingsSection(title = "Устройство") { + OutlinedTextField( + value = deviceName, + onValueChange = { deviceName = it }, + label = { Text("Имя устройства") }, + placeholder = { Text("Android Device") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = GodEyeColors.NavyLight, + unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f), + focusedTextColor = GodEyeColors.IvoryPure, + unfocusedTextColor = GodEyeColors.IvorySoft, + focusedLabelColor = GodEyeColors.NavyLight, + unfocusedLabelColor = GodEyeColors.IvorySoft + ), + leadingIcon = { + Icon( + Icons.Default.Smartphone, + contentDescription = null, + tint = GodEyeColors.NavyLight + ) + } + ) + + Text( + text = "Это имя будет отображаться операторам при подключении", + style = MaterialTheme.typography.bodySmall, + color = GodEyeColors.IvorySoft, + modifier = Modifier.padding(start = 48.dp, top = 4.dp) + ) + } + } + + // Секция "Автоматизация" + item { + SettingsSection(title = "Автоматизация") { + SettingsSwitchCard( + title = "Автоматическое принятие запросов", + subtitle = "Автоматически принимать запросы от операторов", + checked = autoAcceptRequests, + onCheckedChange = { autoAcceptRequests = it }, + icon = Icons.Default.AutoAwesome + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsSwitchCard( + title = "Уведомления", + subtitle = "Показывать уведомления о входящих запросах", + checked = enableNotifications, + onCheckedChange = { enableNotifications = it }, + icon = Icons.Default.Notifications + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsSwitchCard( + title = "Не выключать экран", + subtitle = "Экран остается включенным во время сессии", + checked = keepScreenOn, + onCheckedChange = { keepScreenOn = it }, + icon = Icons.Default.ScreenLockPortrait + ) + } + } + + // Секция "О приложении" + item { + SettingsSection(title = "О приложении") { + InfoCard( + title = "GodEye Android Client", + subtitle = "Версия 1.0.0 (Build 1)", + icon = Icons.Default.Info + ) + + Spacer(modifier = Modifier.height(8.dp)) + + InfoCard( + title = "Device ID", + subtitle = context.getPreferences().getString("device_id", "Неизвестно") ?: "Неизвестно", + icon = Icons.Default.Fingerprint + ) + } + } + } + } +} + +@Composable +fun SettingsSection( + title: String, + content: @Composable ColumnScope.() -> Unit +) { + Column { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = GodEyeColors.IvoryPure, + modifier = Modifier.padding(bottom = 12.dp) + ) + content() + } +} + +@Composable +fun SettingsSwitchCard( + title: String, + subtitle: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + icon: androidx.compose.ui.graphics.vector.ImageVector +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (checked) GodEyeColors.SuccessGreen else GodEyeColors.IvorySoft, + modifier = Modifier.size(24.dp) + ) + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Medium, + color = GodEyeColors.IvoryPure + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = GodEyeColors.IvorySoft + ) + } + + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + checkedThumbColor = GodEyeColors.IvoryPure, + checkedTrackColor = GodEyeColors.SuccessGreen, + uncheckedThumbColor = GodEyeColors.IvorySoft, + uncheckedTrackColor = GodEyeColors.NavyDark + ) + ) + } + } +} + +@Composable +fun InfoCard( + title: String, + subtitle: String, + icon: androidx.compose.ui.graphics.vector.ImageVector +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = GodEyeColors.NavyLight, + modifier = Modifier.size(24.dp) + ) + + Column { + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Medium, + color = GodEyeColors.IvoryPure + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = GodEyeColors.IvorySoft + ) + } + } + } +} diff --git a/app/src/main/java/com/example/godeye/camera/CameraManager.kt b/app/src/main/java/com/example/godeye/camera/CameraManager.kt new file mode 100644 index 0000000..c7930f4 --- /dev/null +++ b/app/src/main/java/com/example/godeye/camera/CameraManager.kt @@ -0,0 +1,236 @@ +package com.example.godeye.camera + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import com.example.godeye.utils.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/** + * CameraManager - безопасное управление камерой для предпросмотра + * Исправлены проблемы с освобождением ресурсов и утечками памяти + */ +class CameraManager(private val context: Context) { + + private var cameraProvider: ProcessCameraProvider? = null + private var camera: Camera? = null + private var preview: Preview? = null + private var imageCapture: ImageCapture? = null + + private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() + private var currentCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + private var isReleased = false + + /** + * Проверка разрешений камеры + */ + fun hasPermissions(): Boolean { + return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED + } + + /** + * Безопасная настройка камеры с предпросмотром + */ + fun setupCamera( + previewView: PreviewView, + lifecycleOwner: LifecycleOwner, + cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + ) { + if (isReleased) { + Logger.error("CAMERA_SETUP_ERROR", "Camera manager already released") + return + } + + try { + // Сначала безопасно освобождаем предыдущие ресурсы + safeCameraCleanup() + + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProvider = cameraProviderFuture.get() + + currentCameraSelector = cameraSelector + + // Создание preview use case с безопасными настройками + preview = Preview.Builder() + .setTargetRotation(previewView.display.rotation) + .build().also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + + // Создание image capture use case с оптимизацией для Android 9 + imageCapture = ImageCapture.Builder() + .setTargetRotation(previewView.display.rotation) + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() + + // Отвязка всех use cases перед привязкой новых + cameraProvider?.unbindAll() + + // Безопасная привязка use cases к lifecycle + camera = cameraProvider?.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview, + imageCapture + ) + + Logger.step("CAMERA_SETUP", "Camera setup completed safely") + + } catch (e: Exception) { + Logger.error("CAMERA_SETUP_ERROR", "Failed to setup camera safely", e) + safeCameraCleanup() + throw e + } + } + + /** + * Безопасная очистка ресурсов камеры + */ + private fun safeCameraCleanup() { + try { + cameraProvider?.unbindAll() + preview = null + imageCapture = null + camera = null + } catch (e: Exception) { + Logger.error("CAMERA_CLEANUP_ERROR", "Error during camera cleanup", e) + } + } + + /** + * Переключение камеры с безопасной обработкой + */ + fun switchCamera(): CameraSelector { + if (isReleased) return currentCameraSelector + + currentCameraSelector = if (currentCameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { + CameraSelector.DEFAULT_FRONT_CAMERA + } else { + CameraSelector.DEFAULT_BACK_CAMERA + } + return currentCameraSelector + } + + /** + * Безопасное включение/выключение вспышки + */ + fun toggleFlash() { + if (isReleased) return + + try { + camera?.let { camera -> + if (camera.cameraInfo.hasFlashUnit()) { + val flashMode = camera.cameraInfo.torchState.value + camera.cameraControl.enableTorch(flashMode == TorchState.OFF) + } + } + } catch (e: Exception) { + Logger.error("FLASH_TOGGLE_ERROR", "Error toggling flash", e) + } + } + + /** + * Безопасная съемка фото + */ + fun takePhoto( + outputFile: File, + onPhotoTaken: (File) -> Unit, + onError: (Exception) -> Unit + ) { + if (isReleased) { + onError(Exception("Camera manager released")) + return + } + + val imageCapture = imageCapture ?: run { + onError(Exception("ImageCapture not initialized")) + return + } + + try { + val outputOptions = ImageCapture.OutputFileOptions.Builder(outputFile).build() + + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(context), + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + onPhotoTaken(outputFile) + Logger.step("PHOTO_TAKEN", "Photo saved safely to: ${outputFile.absolutePath}") + } + + override fun onError(exception: ImageCaptureException) { + onError(exception) + Logger.error("PHOTO_ERROR", "Photo capture failed safely", exception) + } + } + ) + } catch (e: Exception) { + onError(e) + Logger.error("PHOTO_SETUP_ERROR", "Failed to setup photo capture", e) + } + } + + /** + * Начало записи видео - заглушка для совместимости + */ + fun startRecording( + outputFile: File, + onRecordingStarted: () -> Unit, + onError: (Exception) -> Unit + ) { + onError(Exception("Video recording not supported on this device for stability")) + } + + /** + * Остановка записи видео - заглушка для совместимости + */ + fun stopRecording() { + Logger.step("VIDEO_RECORDING_STOPPED", "Video recording stop requested (not supported)") + } + + /** + * Безопасное освобождение ресурсов + */ + fun release() { + if (isReleased) return + + try { + Logger.step("CAMERA_MANAGER_RELEASING", "Starting safe camera manager release") + + isReleased = true + + // Безопасная очистка камеры + safeCameraCleanup() + + // Безопасное завершение executor + cameraExecutor.shutdown() + try { + if (!cameraExecutor.awaitTermination(1, TimeUnit.SECONDS)) { + cameraExecutor.shutdownNow() + } + } catch (e: InterruptedException) { + cameraExecutor.shutdownNow() + Thread.currentThread().interrupt() + } + + cameraProvider = null + + Logger.step("CAMERA_MANAGER_RELEASED", "Camera manager resources released safely") + + } catch (e: Exception) { + Logger.error("CAMERA_RELEASE_ERROR", "Error during camera manager release", e) + } + } +} diff --git a/app/src/main/java/com/example/godeye/camera/CameraScreen.kt b/app/src/main/java/com/example/godeye/camera/CameraScreen.kt new file mode 100644 index 0000000..06d3323 --- /dev/null +++ b/app/src/main/java/com/example/godeye/camera/CameraScreen.kt @@ -0,0 +1,315 @@ +package com.example.godeye.camera + +import android.Manifest +import android.content.Context +import android.view.SurfaceView +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import com.example.godeye.ui.components.* +import com.example.godeye.ui.theme.GodEyeColors +import com.example.godeye.utils.Logger +import kotlinx.coroutines.launch +import java.io.File +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@Composable +fun CameraScreen( + onBackPressed: () -> Unit, + sessionId: String = "", + @Suppress("UNUSED_PARAMETER") operatorId: String = "" +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + var hasPermissions by remember { mutableStateOf(false) } + var showError by remember { mutableStateOf(null) } + + // Упрощенный CameraManager без сложных анимаций + val cameraManager = remember { + try { + CameraManager(context) + } catch (e: Exception) { + Logger.error("CAMERA_MANAGER_CREATE_ERROR", "Failed to create camera manager", e) + null + } + } + + val previewView = remember { + try { + PreviewView(context).apply { + scaleType = PreviewView.ScaleType.FILL_CENTER + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + } + } catch (e: Exception) { + Logger.error("PREVIEW_VIEW_CREATE_ERROR", "Failed to create preview view", e) + null + } + } + + // Проверка разрешений + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + hasPermissions = permissions.values.all { it } + if (!hasPermissions) { + showError = "Необходимы разрешения для работы с камерой" + } + } + + LaunchedEffect(Unit) { + hasPermissions = cameraManager?.hasPermissions() ?: false + if (!hasPermissions) { + permissionLauncher.launch( + arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO + ) + ) + } + } + + // Простая инициализация камеры БЕЗ сложных анимаций + LaunchedEffect(hasPermissions) { + if (hasPermissions && cameraManager != null && previewView != null) { + try { + Logger.step("CAMERA_INIT_START", "Starting simple camera initialization") + cameraManager.setupCamera(previewView, lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA) + Logger.step("CAMERA_INIT_SUCCESS", "Simple camera initialized successfully") + } catch (e: Exception) { + showError = "Ошибка инициализации камеры: ${e.message}" + Logger.error("CAMERA_INIT_ERROR", "Camera initialization failed", e) + } + } + } + + // Безопасное освобождение ресурсов при закрытии + DisposableEffect(cameraManager) { + onDispose { + try { + Logger.step("CAMERA_SCREEN_DISPOSE", "Disposing camera screen safely") + cameraManager?.release() + } catch (e: Exception) { + Logger.error("CAMERA_DISPOSE_ERROR", "Error disposing camera", e) + } + } + } + + // УПРОЩЕННЫЙ UI БЕЗ СЛОЖНЫХ АНИМАЦИЙ + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + ) { + if (hasPermissions && previewView != null && cameraManager != null) { + // Простой preview БЕЗ сложных эффектов + AndroidView( + factory = { previewView }, + modifier = Modifier.fillMaxSize() + ) + + // Простая верхняя панель БЕЗ анимаций + Card( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(16.dp) + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color.Black.copy(alpha = 0.7f) + ), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onBackPressed, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Gray + ) + ) { + Text("← Назад", color = Color.White) + } + + Text( + text = "GodEye Camera", + color = Color.White, + style = MaterialTheme.typography.titleMedium + ) + + Spacer(modifier = Modifier.width(60.dp)) + } + } + + // Простая нижняя панель БЕЗ анимаций + Card( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(16.dp) + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color.Black.copy(alpha = 0.7f) + ), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + // Простая кнопка фото БЕЗ анимаций + Button( + onClick = { + try { + val photoFile = File( + context.externalCacheDir, + "photo_${System.currentTimeMillis()}.jpg" + ) + cameraManager.takePhoto( + photoFile, + onPhotoTaken = { + Logger.step("PHOTO_TAKEN", "Photo taken successfully") + }, + onError = { error -> + showError = "Ошибка съемки: ${error.message}" + } + ) + } catch (e: Exception) { + showError = "Ошибка съемки: ${e.message}" + } + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Blue + ) + ) { + Text("📷 Фото", color = Color.White) + } + + // Простая кнопка переключения камеры БЕЗ анимаций + Button( + onClick = { + try { + val newCameraSelector = cameraManager.switchCamera() + // Простое пересоздание камеры с новым селектором + cameraManager.setupCamera(previewView, lifecycleOwner, newCameraSelector) + } catch (e: Exception) { + showError = "Ошибка переключения камеры" + } + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Green + ) + ) { + Text("🔄 Камера", color = Color.White) + } + } + } + } else { + // Простой экран разрешений БЕЗ анимаций + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = if (cameraManager == null || previewView == null) "Ошибка камеры" else "Требуются разрешения", + style = MaterialTheme.typography.headlineMedium, + color = Color.White, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(24.dp)) + + if (cameraManager != null && previewView != null) { + Button( + onClick = { + permissionLauncher.launch( + arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO + ) + ) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Blue + ) + ) { + Text("Предоставить разрешения", color = Color.White) + } + } else { + Button( + onClick = onBackPressed, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Red + ) + ) { + Text("Вернуться назад", color = Color.White) + } + } + } + } + + // Простое отображение ошибок БЕЗ анимаций + showError?.let { error -> + Card( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(16.dp) + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color.Red.copy(alpha = 0.9f) + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = error, + color = Color.White, + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { showError = null }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White.copy(alpha = 0.2f) + ) + ) { + Text("Закрыть", color = Color.White) + } + } + } + } + } +} diff --git a/app/src/main/java/com/example/godeye/managers/Camera2Manager.kt b/app/src/main/java/com/example/godeye/managers/Camera2Manager.kt new file mode 100644 index 0000000..cef5975 --- /dev/null +++ b/app/src/main/java/com/example/godeye/managers/Camera2Manager.kt @@ -0,0 +1,303 @@ +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.asStateFlow() + + private val _availableCameras = MutableStateFlow>(emptyList()) + val availableCameras: StateFlow> = _availableCameras.asStateFlow() + + init { + detectAvailableCameras() + } + + /** + * Определение доступных камер устройства согласно ТЗ + * Поддерживает: back, front, wide, telephoto + */ + private fun detectAvailableCameras() { + Logger.step("CAMERA_DETECTION", "Detecting available cameras") + + val cameras = mutableListOf() + + 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 { + 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) + } + } + + 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() + } +} diff --git a/app/src/main/java/com/example/godeye/managers/CameraManager.kt b/app/src/main/java/com/example/godeye/managers/CameraManager.kt deleted file mode 100644 index b2eb7e4..0000000 --- a/app/src/main/java/com/example/godeye/managers/CameraManager.kt +++ /dev/null @@ -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 = _isRecording.asStateFlow() - - private val _currentCameraType = MutableStateFlow(null) - val currentCameraType: StateFlow = _currentCameraType.asStateFlow() - - private val _error = MutableStateFlow(null) - val error: StateFlow = _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 { - 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 - } -} diff --git a/app/src/main/java/com/example/godeye/managers/PermissionManager.kt b/app/src/main/java/com/example/godeye/managers/PermissionManager.kt index 9fd5ca0..63141dd 100644 --- a/app/src/main/java/com/example/godeye/managers/PermissionManager.kt +++ b/app/src/main/java/com/example/godeye/managers/PermissionManager.kt @@ -4,99 +4,218 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import androidx.core.content.ContextCompat +import com.example.godeye.models.AppError 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) { + private val _permissionsGranted = MutableStateFlow(false) + val permissionsGranted: StateFlow = _permissionsGranted.asStateFlow() + + private val _missingPermissions = MutableStateFlow>(emptyList()) + val missingPermissions: StateFlow> = _missingPermissions.asStateFlow() + 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.FOREGROUND_SERVICE, - Manifest.permission.POST_NOTIFICATIONS - ) - - val CAMERA_PERMISSIONS = arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.RECORD_AUDIO - ) - } - - /** - * Проверить, есть ли все необходимые разрешения - */ - fun hasAllRequiredPermissions(): Boolean { - return REQUIRED_PERMISSIONS.all { permission -> - ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED + Manifest.permission.FOREGROUND_SERVICE + ).apply { + // Добавляем FOREGROUND_SERVICE_CAMERA для API 34+ + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + plus(Manifest.permission.FOREGROUND_SERVICE_CAMERA) + } } - } - /** - * Проверить разрешения для камеры - */ - 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 { - return REQUIRED_PERMISSIONS.filter { permission -> - ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED - } - } - - /** - * Получить список отсутствующих разрешений для камеры - */ - fun getMissingCameraPermissions(): List { - return CAMERA_PERMISSIONS.filter { permission -> - ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED - } - } - - /** - * Проверить критические разрешения для основной функциональности - */ - fun hasCriticalPermissions(): Boolean { - val criticalPermissions = arrayOf( + /** + * Критически важные разрешения для основной функциональности + */ + val CRITICAL_PERMISSIONS = arrayOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, 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() + + 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 { + return REQUIRED_PERMISSIONS.filter { !isPermissionGranted(it) }.toTypedArray() + } + + /** + * Получение списка критически важных отсутствующих разрешений + */ + fun getMissingCriticalPermissions(): Array { + return CRITICAL_PERMISSIONS.filter { !isPermissionGranted(it) }.toTypedArray() + } + + /** + * Обработка результата запроса разрешений + */ + fun onPermissionsResult( + permissions: Array, + grantResults: IntArray + ): PermissionResult { + Logger.step("PERMISSION_RESULT", "Processing permission request result") + + val granted = mutableListOf() + val denied = mutableListOf() + + 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() { - Logger.d("=== Permission Status ===") - REQUIRED_PERMISSIONS.forEach { permission -> - val granted = hasPermission(permission) - Logger.d("$permission: ${if (granted) "GRANTED" else "DENIED"}") + fun getPermissionDescription(permission: String): String { + return when (permission) { + Manifest.permission.CAMERA -> "Доступ к камере для видеосвязи" + Manifest.permission.RECORD_AUDIO -> "Доступ к микрофону для аудиосвязи" + Manifest.permission.INTERNET -> "Доступ к интернету для подключения к серверу" + Manifest.permission.ACCESS_NETWORK_STATE -> "Проверка состояния сети" + Manifest.permission.WAKE_LOCK -> "Предотвращение засыпания устройства" + Manifest.permission.FOREGROUND_SERVICE -> "Работа в фоновом режиме" + Manifest.permission.FOREGROUND_SERVICE_CAMERA -> "Фоновая работа с камерой" + else -> "Системное разрешение" + } + } + + /** + * Проверка необходимости объяснения разрешения + */ + fun shouldShowRationale(permission: String): Boolean { + // Для системных разрешений обычно не показываем rationale + return when (permission) { + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO -> true + else -> false } - Logger.d("All required permissions: ${hasAllRequiredPermissions()}") - Logger.d("Camera permissions: ${hasCameraPermissions()}") - Logger.d("Critical permissions: ${hasCriticalPermissions()}") } } + +/** + * Результат запроса разрешений + */ +sealed class PermissionResult { + object AllGranted : PermissionResult() + data class SomeGranted(val granted: List, val denied: List) : PermissionResult() + data class CriticalDenied(val denied: List) : PermissionResult() +} diff --git a/app/src/main/java/com/example/godeye/managers/SessionManager.kt b/app/src/main/java/com/example/godeye/managers/SessionManager.kt index ed5e776..7687eff 100644 --- a/app/src/main/java/com/example/godeye/managers/SessionManager.kt +++ b/app/src/main/java/com/example/godeye/managers/SessionManager.kt @@ -1,24 +1,33 @@ package com.example.godeye.managers -import com.example.godeye.models.CameraSession +import com.example.godeye.models.* 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.ConcurrentHashMap /** - * Менеджер для управления активными сессиями с операторами + * SessionManager - управление активными сессиями с операторами + * Отслеживает состояние WebRTC соединений и сессий камеры */ class SessionManager { - private val _activeSessions = MutableStateFlow>(emptyList()) - val activeSessions: StateFlow> = _activeSessions.asStateFlow() + private val activeSessions = ConcurrentHashMap() + + private val _sessions = MutableStateFlow>(emptyMap()) + val sessions: StateFlow> = _sessions.asStateFlow() + + private val _activeSessionCount = MutableStateFlow(0) + val activeSessionCount: StateFlow = _activeSessionCount.asStateFlow() /** - * Добавить новую сессию + * Создание новой сессии при принятии запроса оператора */ - fun addSession(sessionId: String, operatorId: String, cameraType: String) { - val newSession = CameraSession( + fun createSession(sessionId: String, operatorId: String, cameraType: String): CameraSession { + Logger.step("SESSION_CREATE", "Creating session: $sessionId for operator $operatorId") + + val session = CameraSession( sessionId = sessionId, operatorId = operatorId, cameraType = cameraType, @@ -27,126 +36,121 @@ class SessionManager { webRTCConnected = false ) - val currentSessions = _activeSessions.value.toMutableList() - // Удаляем существующую сессию с тем же ID, если есть - currentSessions.removeAll { it.sessionId == sessionId } - currentSessions.add(newSession) - _activeSessions.value = currentSessions + activeSessions[sessionId] = session + updateSessionsFlow() - Logger.d("Session added: $sessionId, operator: $operatorId, camera: $cameraType") + Logger.step("SESSION_CREATED", "Session created: $sessionId") + return session } /** - * Обновить статус WebRTC соединения для сессии + * Обновление статуса WebRTC соединения для сессии */ - fun updateWebRTCStatus(sessionId: String, connected: Boolean) { - val currentSessions = _activeSessions.value.toMutableList() - val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId } + fun updateWebRTCConnection(sessionId: String, connected: Boolean) { + activeSessions[sessionId]?.let { session -> + session.webRTCConnected = connected + activeSessions[sessionId] = session + updateSessionsFlow() - if (sessionIndex != -1) { - currentSessions[sessionIndex] = currentSessions[sessionIndex].copy( - webRTCConnected = connected - ) - _activeSessions.value = currentSessions - Logger.d("WebRTC status updated for session $sessionId: $connected") + Logger.step("SESSION_WEBRTC_UPDATED", + "Session $sessionId WebRTC status updated: $connected") } } /** - * Переключить камеру для сессии + * Завершение сессии */ - fun switchCameraForSession(sessionId: String, newCameraType: String) { - val currentSessions = _activeSessions.value.toMutableList() - val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId } + fun endSession(sessionId: String, reason: String = "User ended") { + activeSessions[sessionId]?.let { session -> + session.isActive = false + activeSessions.remove(sessionId) + updateSessionsFlow() - if (sessionIndex != -1) { - currentSessions[sessionIndex] = currentSessions[sessionIndex].copy( - cameraType = newCameraType - ) - _activeSessions.value = currentSessions - Logger.d("Camera switched for session $sessionId to $newCameraType") + Logger.step("SESSION_ENDED", "Session ended: $sessionId, reason: $reason") } } /** - * Завершить сессию - */ - 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 + * Получение активной сессии по ID */ fun getSession(sessionId: String): CameraSession? { - return _activeSessions.value.find { it.sessionId == sessionId } + return activeSessions[sessionId] } /** - * Проверить, есть ли активные сессии + * Получение всех активных сессий + */ + fun getAllActiveSessions(): List { + return activeSessions.values.filter { it.isActive } + } + + /** + * Проверка, есть ли активные сессии */ fun hasActiveSessions(): Boolean { - return _activeSessions.value.isNotEmpty() + return activeSessions.values.any { it.isActive } } /** - * Получить количество активных сессий + * Завершение всех активных сессий */ - fun getActiveSessionCount(): Int { - return _activeSessions.value.size + fun endAllSessions(reason: String = "Service stopped") { + Logger.step("SESSION_END_ALL", "Ending all active sessions: $reason") + + activeSessions.values.forEach { session -> + if (session.isActive) { + session.isActive = false + Logger.step("SESSION_ENDED", "Session ended: ${session.sessionId}") + } + } + + activeSessions.clear() + updateSessionsFlow() } /** - * Завершить все сессии + * Переключение камеры для сессии */ - fun endAllSessions() { - val sessionIds = _activeSessions.value.map { it.sessionId } - _activeSessions.value = emptyList() - Logger.d("All sessions ended: ${sessionIds.joinToString(", ")}") + fun switchCamera(sessionId: String, newCameraType: String) { + activeSessions[sessionId]?.let { session -> + session.cameraType = newCameraType + activeSessions[sessionId] = session + updateSessionsFlow() + + Logger.step("SESSION_CAMERA_SWITCHED", + "Session $sessionId camera switched to: $newCameraType") + } } /** - * Получить текущий тип камеры для активной сессии - */ - fun 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 + val active = activeSessions.values.filter { it.isActive } + val withWebRTC = active.filter { it.webRTCConnected } + 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 } + totalActive = active.size, + webRTCConnected = withWebRTC.size, + operators = active.map { it.operatorId }.distinct().size, + averageDuration = if (active.isNotEmpty()) { + active.map { System.currentTimeMillis() - it.startTime }.average().toLong() + } else 0L ) } + + private fun updateSessionsFlow() { + _sessions.value = activeSessions.toMap() + _activeSessionCount.value = activeSessions.values.count { it.isActive } + } } /** * Статистика сессий */ data class SessionStats( - val totalSessions: Int, - val connectedSessions: Int, - val activeSessions: Int, - val oldestSessionTime: Long?, - val newestSessionTime: Long? + val totalActive: Int, + val webRTCConnected: Int, + val operators: Int, + val averageDuration: Long ) diff --git a/app/src/main/java/com/example/godeye/managers/WebRTCManager.kt b/app/src/main/java/com/example/godeye/managers/WebRTCManager.kt deleted file mode 100644 index 34eb98a..0000000 --- a/app/src/main/java/com/example/godeye/managers/WebRTCManager.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.example.godeye.managers - -import android.content.Context -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.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -/** - * Менеджер для управления WebRTC соединениями (заглушка) - * В реальном проекте здесь будет полная реализация с WebRTC библиотекой - */ -class WebRTCManager(private val context: Context) { - - private val _connectionState = MutableStateFlow(WebRTCConnectionState.NEW) - val connectionState: StateFlow = _connectionState.asStateFlow() - - private val _error = MutableStateFlow(null) - val error: StateFlow = _error.asStateFlow() - - // Callback для передачи событий WebRTC - private var onOfferCreated: ((String) -> Unit)? = null - private var onAnswerCreated: ((String) -> Unit)? = null - private var onIceCandidateCreated: ((String, String, Int) -> Unit)? = null - - /** - * Инициализация WebRTC (заглушка) - */ - fun initialize() { - try { - Logger.d("WebRTC initialized (stub implementation)") - } catch (e: Exception) { - Logger.e("Error initializing WebRTC", e) - _error.value = AppError.WebRTCConnectionFailed - } - } - - /** - * Создать PeerConnection (заглушка) - */ - fun createPeerConnection( - onOfferCreated: (String) -> Unit, - onAnswerCreated: (String) -> Unit, - onIceCandidateCreated: (String, String, Int) -> Unit - ) { - try { - this.onOfferCreated = onOfferCreated - this.onAnswerCreated = onAnswerCreated - this.onIceCandidateCreated = onIceCandidateCreated - - Logger.d("PeerConnection created (stub implementation)") - } catch (e: Exception) { - Logger.e("Error creating PeerConnection", e) - _error.value = AppError.WebRTCConnectionFailed - } - } - - /** - * Создать локальные медиа треки (заглушка) - */ - fun createLocalMediaTracks(cameraType: String) { - try { - Logger.d("Local media tracks created for $cameraType (stub implementation)") - } catch (e: Exception) { - Logger.e("Error creating local media tracks", e) - _error.value = AppError.WebRTCConnectionFailed - } - } - - /** - * Создать Offer (заглушка) - */ - fun createOffer() { - try { - // Симулируем создание offer - val mockOffer = "v=0\r\no=- 123456 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 127.0.0.1\r\na=rtcp:9 IN IP4 127.0.0.1" - onOfferCreated?.invoke(mockOffer) - Logger.d("Offer created (stub implementation)") - - // Симулируем успешное соединение через некоторое время - _connectionState.value = WebRTCConnectionState.CONNECTED - } catch (e: Exception) { - Logger.e("Error creating offer", e) - _error.value = AppError.WebRTCConnectionFailed - } - } - - /** - * Обработать Answer (заглушка) - */ - fun handleAnswer(answerSdp: String) { - try { - Logger.d("Answer processed (stub implementation): ${answerSdp.take(50)}...") - _connectionState.value = WebRTCConnectionState.CONNECTED - } catch (e: Exception) { - Logger.e("Error handling answer", e) - _error.value = AppError.WebRTCConnectionFailed - } - } - - /** - * Добавить ICE candidate (заглушка) - */ - fun addIceCandidate(candidateSdp: String, sdpMid: String, sdpMLineIndex: Int) { - try { - Logger.d("ICE candidate added (stub implementation): $candidateSdp") - } catch (e: Exception) { - Logger.e("Error adding ICE candidate", e) - } - } - - /** - * Переключить камеру (заглушка) - */ - fun switchCamera(newCameraType: String) { - try { - Logger.d("Camera switched to: $newCameraType (stub implementation)") - } catch (e: Exception) { - Logger.e("Error switching camera", e) - _error.value = AppError.CameraError("Failed to switch camera: ${e.message}") - } - } - - /** - * Закрыть WebRTC соединение (заглушка) - */ - fun close() { - try { - _connectionState.value = WebRTCConnectionState.CLOSED - Logger.d("WebRTC connection closed (stub implementation)") - } catch (e: Exception) { - Logger.e("Error closing WebRTC connection", e) - } - } - - /** - * Очистить ошибку - */ - fun clearError() { - _error.value = null - } -} diff --git a/app/src/main/java/com/example/godeye/models/CameraModels.kt b/app/src/main/java/com/example/godeye/models/CameraModels.kt new file mode 100644 index 0000000..3a4620e --- /dev/null +++ b/app/src/main/java/com/example/godeye/models/CameraModels.kt @@ -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, // Поддерживаемые разрешения + val focalLengths: List // Фокусные расстояния для определения типа +) diff --git a/app/src/main/java/com/example/godeye/models/Models.kt b/app/src/main/java/com/example/godeye/models/Models.kt index caa27f1..dd66b60 100644 --- a/app/src/main/java/com/example/godeye/models/Models.kt +++ b/app/src/main/java/com/example/godeye/models/Models.kt @@ -1,154 +1,55 @@ 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 -) - -/** - * Активная сессия с оператором - */ -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 { DISCONNECTED, CONNECTING, CONNECTED, - ERROR, - RECONNECTING + RECONNECTING, + ERROR } -/** - * Состояния WebRTC соединения - */ -enum class WebRTCConnectionState { - NEW, - 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 = emptyList(), - val isLoading: Boolean = false, - val error: AppError? = null, - val showCameraRequest: CameraRequest? = null +data class CameraResponse( + val sessionId: String, + val accepted: Boolean, + val reason: String? = null, + val streamUrl: String? = 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" +} diff --git a/app/src/main/java/com/example/godeye/models/SocketEvents.kt b/app/src/main/java/com/example/godeye/models/SocketEvents.kt new file mode 100644 index 0000000..5d814d8 --- /dev/null +++ b/app/src/main/java/com/example/godeye/models/SocketEvents.kt @@ -0,0 +1,62 @@ +package com.example.godeye.models + +/** + * Информация об Android устройстве для регистрации на сервере + * Соответствует требованиям ТЗ для Socket.IO регистрации + */ +data class DeviceInfo( + val model: String, + val androidVersion: String, + val appVersion: String, + val availableCameras: List +) + +/** + * Активная сессия камеры с оператором + * Соответствует требованиям ТЗ для управления 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() +} diff --git a/app/src/main/java/com/example/godeye/services/CameraService.kt b/app/src/main/java/com/example/godeye/services/CameraService.kt deleted file mode 100644 index 25b3ac1..0000000 --- a/app/src/main/java/com/example/godeye/services/CameraService.kt +++ /dev/null @@ -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 = _isActive.asStateFlow() - - private val _error = MutableStateFlow(null) - val error: StateFlow = _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") - } -} diff --git a/app/src/main/java/com/example/godeye/services/SocketService.kt b/app/src/main/java/com/example/godeye/services/SocketService.kt index 1050e9d..76c7b4d 100644 --- a/app/src/main/java/com/example/godeye/services/SocketService.kt +++ b/app/src/main/java/com/example/godeye/services/SocketService.kt @@ -1,19 +1,17 @@ package com.example.godeye.services -import android.app.* -import android.content.Context +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service import android.content.Intent import android.os.Binder +import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat -import com.example.godeye.MainActivity import com.example.godeye.R -import com.example.godeye.managers.PermissionManager import com.example.godeye.models.* -import com.example.godeye.utils.Constants 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.JsonObject import io.socket.client.IO @@ -25,11 +23,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.json.JSONArray import org.json.JSONObject import java.net.URI /** - * Сервис для управления WebSocket соединением с backend сервером + * SocketService - основной сервис для WebSocket соединения с backend сервером + * Работает в фоне и обеспечивает постоянное соединение с сервером */ class SocketService : Service() { @@ -38,393 +38,359 @@ class SocketService : Service() { private val gson = Gson() private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private lateinit var permissionManager: PermissionManager - - // StateFlows для отслеживания состояния + // Состояния сервиса private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED) val connectionState: StateFlow = _connectionState.asStateFlow() private val _deviceId = MutableStateFlow("") val deviceId: StateFlow = _deviceId.asStateFlow() - private val _error = MutableStateFlow(null) - val error: StateFlow = _error.asStateFlow() + // События для передачи в UI + private val _cameraRequests = MutableStateFlow(null) + val cameraRequests: StateFlow = _cameraRequests.asStateFlow() - // События для UI - private val _cameraRequest = MutableStateFlow(null) - val cameraRequest: StateFlow = _cameraRequest.asStateFlow() - - private val _webrtcOffer = MutableStateFlow(null) - val webrtcOffer: StateFlow = _webrtcOffer.asStateFlow() - - private val _webrtcAnswer = MutableStateFlow(null) - val webrtcAnswer: StateFlow = _webrtcAnswer.asStateFlow() - - private val _webrtcIceCandidate = MutableStateFlow(null) - val webrtcIceCandidate: StateFlow = _webrtcIceCandidate.asStateFlow() - - private val _cameraSwitchRequest = MutableStateFlow?>(null) // sessionId, newCameraType - val cameraSwitchRequest: StateFlow?> = _cameraSwitchRequest.asStateFlow() - - private val _sessionDisconnect = MutableStateFlow(null) // sessionId - val sessionDisconnect: StateFlow = _sessionDisconnect.asStateFlow() + private val _webRTCEvents = MutableStateFlow(null) + val webRTCEvents: StateFlow = _webRTCEvents.asStateFlow() inner class LocalBinder : Binder() { fun getService(): SocketService = this@SocketService } + override fun onBind(intent: Intent): IBinder = binder + override fun onCreate() { super.onCreate() - Logger.d("SocketService created") - permissionManager = PermissionManager(this) - _deviceId.value = generateDeviceId() + Logger.step("SOCKET_SERVICE_CREATE", "SocketService created") createNotificationChannel() + startForeground(NOTIFICATION_ID, createNotification()) } - override fun onBind(intent: Intent?): IBinder = binder - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - startForeground(Constants.FOREGROUND_SERVICE_ID, createNotification()) - return START_STICKY + Logger.step("SOCKET_SERVICE_START", "SocketService started") + return START_STICKY // Перезапускать сервис при убийстве системой } /** - * Подключиться к серверу + * Подключение к backend серверу по Socket.IO */ - fun connect(serverUrl: String) { + fun connect(serverUrl: String, deviceId: String) { + Logger.step("SOCKET_CONNECT", "Connecting to server: $serverUrl") + _deviceId.value = deviceId + _connectionState.value = ConnectionState.CONNECTING + serviceScope.launch { try { - _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 секунд + timeout = 10000 reconnection = true - reconnectionDelay = 2000 // Увеличиваем задержку между попытками - reconnectionAttempts = 3 // Уменьшаем количество попыток - forceNew = true // Принудительно создаваем новое соединение + reconnectionAttempts = 5 + reconnectionDelay = 1000 } - 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() - } - } + socket = IO.socket(uri, options) + setupEventListeners() + socket?.connect() } catch (e: Exception) { - Logger.e("Error connecting to server: ${e.message}", e) + Logger.error("SOCKET_CONNECT_ERROR", "Failed to connect to server", e) _connectionState.value = ConnectionState.ERROR - _error.value = AppError.SocketError("Connection failed: ${e.message}") } } } /** - * Отключиться от сервера - */ - fun disconnect() { - serviceScope.launch { - try { - socket?.disconnect() - socket?.close() - socket = null - _connectionState.value = ConnectionState.DISCONNECTED - Logger.d("Disconnected from server") - } catch (e: Exception) { - Logger.e("Error disconnecting from server", e) - } - } - } - - /** - * Настроить обработчики событий Socket.IO + * Настройка обработчиков событий Socket.IO */ private fun setupEventListeners() { socket?.apply { - Logger.d("Setting up Socket.IO event listeners") - + // Событие подключения on(Socket.EVENT_CONNECT) { - Logger.d("✅ Socket connected successfully") + Logger.step("SOCKET_CONNECTED", "Connected to server") _connectionState.value = ConnectionState.CONNECTED registerDevice() } - on(Socket.EVENT_DISCONNECT) { args -> - val reason = args.firstOrNull()?.toString() ?: "unknown" - Logger.d("❌ Socket disconnected: $reason") + // Событие отключения + on(Socket.EVENT_DISCONNECT) { + Logger.step("SOCKET_DISCONNECTED", "Disconnected from server") _connectionState.value = ConnectionState.DISCONNECTED } - on(Socket.EVENT_CONNECT_ERROR) { args -> - val error = args.firstOrNull()?.toString() ?: "Unknown connection error" - Logger.e("🔥 Socket connection error: $error") - _connectionState.value = ConnectionState.ERROR - _error.value = AppError.SocketError(error) + // Событие подключения (исправлено) + on(Socket.EVENT_CONNECT) { + Logger.step("SOCKET_RECONNECTED", "Reconnected to server") + _connectionState.value = ConnectionState.CONNECTED + registerDevice() } - on(Constants.SocketEvents.REGISTER_SUCCESS) { args -> - Logger.d("Device registered successfully") - val data = args.firstOrNull()?.toString() - Logger.d("Registration response: $data") + // Успешная регистрация устройства + on("register:success") { args -> + Logger.step("REGISTER_SUCCESS", "Device registered successfully") + val response = args[0] as JSONObject + Logger.d("Registration response: $response") } - on(Constants.SocketEvents.REGISTER_ERROR) { args -> - val error = args.firstOrNull()?.toString() ?: "Registration failed" - Logger.e("Device registration error: $error") - _error.value = AppError.SocketError(error) - } - - on(Constants.SocketEvents.CAMERA_REQUEST) { args -> + // Запрос доступа к камере от оператора + on("camera:request") { args -> try { - val data = JSONObject(args[0].toString()) - val request = CameraRequest( - sessionId = data.getString("sessionId"), - operatorId = data.getString("operatorId"), - cameraType = data.getString("cameraType") + val requestData = args[0] as JSONObject + val cameraRequest = CameraRequest( + sessionId = requestData.getString("sessionId"), + operatorId = requestData.getString("operatorId"), + 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) { - 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 { - val data = JSONObject(args[0].toString()) + val data = args[0] as JSONObject val sessionId = data.getString("sessionId") - Logger.d("Camera disconnect received for session: $sessionId") - _sessionDisconnect.value = sessionId + Logger.step("CAMERA_DISCONNECT", "Session $sessionId ended by operator") + + // Очистить текущий запрос если он совпадает + if (_cameraRequests.value?.sessionId == sessionId) { + _cameraRequests.value = null + } + } 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 { - val data = JSONObject(args[0].toString()) - val sessionId = data.getString("sessionId") - 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( + val data = args[0] as JSONObject + val event = WebRTCEvent.Offer( sessionId = data.getString("sessionId"), - type = "offer", - sdp = data.getString("offer") + offer = 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) { - 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 { - val data = JSONObject(args[0].toString()) - val message = WebRTCMessage( + val data = args[0] as JSONObject + 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"), - type = "ice-candidate", candidate = data.getString("candidate"), sdpMid = data.getString("sdpMid"), 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) { - 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() { - try { - val cameraManager = getSystemService(Context.CAMERA_SERVICE) as android.hardware.camera2.CameraManager - val deviceInfo = DeviceInfo( - availableCameras = cameraManager.getAvailableCameraTypes() - ) - - val registrationData = JsonObject().apply { - addProperty("deviceId", _deviceId.value) - add("deviceInfo", gson.toJsonTree(deviceInfo)) - } - - socket?.emit(Constants.SocketEvents.REGISTER_ANDROID, registrationData) - Logger.d("Device registration sent: $registrationData") - - } 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) { - try { - val response = JsonObject().apply { - addProperty("sessionId", sessionId) - addProperty("accepted", accepted) - reason?.let { addProperty("reason", it) } - } - - socket?.emit(Constants.SocketEvents.CAMERA_RESPONSE, response) - Logger.d("Camera response sent: $response") - - } catch (e: Exception) { - Logger.e("Error sending camera response", e) - _error.value = AppError.SocketError("Failed to send camera response: ${e.message}") - } - } - - /** - * Отправить WebRTC answer - */ - fun sendWebRTCAnswer(sessionId: String, answer: String) { - try { - val data = JsonObject().apply { - addProperty("sessionId", sessionId) - addProperty("answer", answer) - } - - socket?.emit(Constants.SocketEvents.WEBRTC_ANSWER, data) - Logger.d("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 - */ - fun sendIceCandidate(sessionId: String, candidate: String, sdpMid: String, sdpMLineIndex: Int) { - try { - val data = JsonObject().apply { - addProperty("sessionId", sessionId) - addProperty("candidate", candidate) - addProperty("sdpMid", sdpMid) - addProperty("sdpMLineIndex", sdpMLineIndex) - } - - socket?.emit(Constants.SocketEvents.WEBRTC_ICE_CANDIDATE, data) - Logger.d("ICE candidate sent for session: $sessionId") - - } catch (e: Exception) { - Logger.e("Error sending ICE candidate", e) - } - } - - /** - * Создать канал уведомлений - */ - private fun createNotificationChannel() { - val channel = NotificationChannel( - Constants.NOTIFICATION_CHANNEL_ID, - "GodEye 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 deviceInfo = DeviceInfo( + model = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}", + androidVersion = android.os.Build.VERSION.RELEASE, + appVersion = "1.0.0", + availableCameras = listOf("back", "front", "ultra_wide", "telephoto") // Получить из CameraManager ) - val statusText = when (_connectionState.value) { - ConnectionState.CONNECTED -> "Подключено" - ConnectionState.CONNECTING -> "Подключение..." - ConnectionState.RECONNECTING -> "Переподключение..." - ConnectionState.DISCONNECTED -> "Отключено" - ConnectionState.ERROR -> "Ошибка подключения" + val registerData = JSONObject().apply { + put("deviceId", _deviceId.value) + put("deviceInfo", JSONObject().apply { + put("model", deviceInfo.model) + put("androidVersion", deviceInfo.androidVersion) + put("appVersion", deviceInfo.appVersion) + put("availableCameras", JSONArray().apply { + deviceInfo.availableCameras.forEach { put(it) } + }) + }) } - return NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_ID) - .setContentTitle("GodEye Signal Center") - .setContentText("Статус: $statusText") + socket?.emit("register:android", registerData) + Logger.step("REGISTER_DEVICE", "Device registration sent: ${deviceInfo.model}") + } + + /** + * Отправка ответа на запрос камеры + */ + fun sendCameraResponse(sessionId: String, accepted: Boolean, reason: String = "") { + val responseData = JSONObject().apply { + put("sessionId", sessionId) + put("accepted", accepted) + if (!accepted && reason.isNotEmpty()) { + put("reason", reason) + } + } + + socket?.emit("camera:response", responseData) + Logger.step("CAMERA_RESPONSE_SENT", "Camera response sent: sessionId=$sessionId, accepted=$accepted") + } + + /** + * Отправка 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) { + val answerData = JSONObject().apply { + put("sessionId", sessionId) + put("answer", answer) + } + + socket?.emit("webrtc:answer", answerData) + Logger.step("WEBRTC_ANSWER_SENT", "WebRTC answer sent for session $sessionId") + } + + /** + * Отправка ICE кандидата + */ + fun sendIceCandidate(sessionId: String, candidate: String, sdpMid: String, sdpMLineIndex: Int) { + val candidateData = JSONObject().apply { + put("sessionId", sessionId) + put("candidate", candidate) + put("sdpMid", sdpMid) + put("sdpMLineIndex", sdpMLineIndex) + } + + socket?.emit("webrtc:ice-candidate", candidateData) + Logger.step("WEBRTC_ICE_SENT", "ICE candidate sent for session $sessionId") + } + + /** + * Отключение от сервера + */ + fun disconnect() { + Logger.step("SOCKET_DISCONNECT", "Disconnecting from server") + socket?.disconnect() + socket = null + _connectionState.value = ConnectionState.DISCONNECTED + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + "GodEye Service", + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "Сервис подключения к серверу" + setShowBadge(false) + } + + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(channel) + } + } + + private fun createNotification(): Notification { + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("GodEye") + .setContentText("Подключено к серверу") .setSmallIcon(R.drawable.ic_launcher_foreground) - .setContentIntent(pendingIntent) .setOngoing(true) - .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + .setCategory(NotificationCompat.CATEGORY_SERVICE) .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() { - super.onDestroy() + Logger.step("SOCKET_SERVICE_DESTROY", "SocketService destroyed") 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 { + 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() +} diff --git a/app/src/main/java/com/example/godeye/streaming/HLSStreamManager.kt b/app/src/main/java/com/example/godeye/streaming/HLSStreamManager.kt new file mode 100644 index 0000000..536f69c --- /dev/null +++ b/app/src/main/java/com/example/godeye/streaming/HLSStreamManager.kt @@ -0,0 +1,272 @@ +package com.example.godeye.streaming + +import com.example.godeye.utils.Logger +import java.io.* +import java.net.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.ConcurrentLinkedQueue + +/** + * HLS Stream Manager - создает HTTP Live Streaming сервер на Android устройстве + * Позволяет операторам подключаться через http://device_ip:8080/hls/stream.m3u8 + */ +class HLSStreamManager { + + private var httpServer: ServerSocket? = null + private var isServerRunning = AtomicBoolean(false) + private var serverThread: Thread? = null + + private val serverPort = 8080 + private var deviceIP: String? = null + private val segmentQueue = ConcurrentLinkedQueue() + private var segmentCounter = 0 + + init { + detectDeviceIP() + } + + private fun detectDeviceIP() { + try { + val interfaces = NetworkInterface.getNetworkInterfaces() + while (interfaces.hasMoreElements()) { + val networkInterface = interfaces.nextElement() + if (!networkInterface.isLoopback && networkInterface.isUp) { + val addresses = networkInterface.inetAddresses + while (addresses.hasMoreElements()) { + val address = addresses.nextElement() + if (!address.isLoopbackAddress && address.isSiteLocalAddress) { + deviceIP = address.hostAddress + break + } + } + } + } + } catch (e: Exception) { + Logger.error("HLS_IP_DETECTION", "Failed to detect IP", e) + } + } + + fun startStreaming(cameraType: String = "back"): String? { + Logger.step("HLS_START_STREAMING", "🎬 Starting HLS server on port $serverPort") + + try { + if (isServerRunning.get()) { + Logger.step("HLS_ALREADY_RUNNING", "HLS server already running") + return "http://$deviceIP:$serverPort/hls/stream.m3u8" + } + + httpServer = ServerSocket(serverPort) + isServerRunning.set(true) + + serverThread = Thread { + while (isServerRunning.get()) { + try { + val clientSocket = httpServer?.accept() + if (clientSocket != null) { + handleHTTPClient(clientSocket) + } + } catch (e: Exception) { + if (isServerRunning.get()) { + Logger.error("HLS_CLIENT_ERROR", "Error handling HTTP client", e) + } + } + } + } + serverThread?.start() + + startSegmentGeneration() + + val hlsUrl = "http://$deviceIP:$serverPort/hls/stream.m3u8" + Logger.step("HLS_SERVER_STARTED", "✅ HLS server started: $hlsUrl") + return hlsUrl + + } catch (e: Exception) { + Logger.error("HLS_START_ERROR", "Failed to start HLS server", e) + return null + } + } + + private fun handleHTTPClient(clientSocket: Socket) { + Thread { + try { + val input = BufferedReader(InputStreamReader(clientSocket.getInputStream())) + val output = PrintWriter(clientSocket.getOutputStream(), true) + + val requestLine = input.readLine() + Logger.step("HLS_REQUEST", "📡 HTTP request: $requestLine") + + // Читаем остальные заголовки + var line: String? + while (input.readLine().also { line = it } != null && line!!.isNotEmpty()) { + // Пропускаем заголовки + } + + when { + requestLine.contains("GET /hls/stream.m3u8") -> { + sendM3U8Playlist(output) + } + requestLine.contains("GET /hls/segment") -> { + val segmentNumber = extractSegmentNumber(requestLine) + sendSegment(output, segmentNumber) + } + requestLine.contains("GET /") -> { + sendCORSHeaders(output) + } + else -> { + send404(output) + } + } + + } catch (e: Exception) { + Logger.error("HLS_CLIENT_HANDLER", "Error handling HTTP client", e) + } finally { + clientSocket.close() + } + }.start() + } + + private fun sendM3U8Playlist(output: PrintWriter) { + val playlist = generateM3U8Playlist() + + output.println("HTTP/1.1 200 OK") + output.println("Content-Type: application/vnd.apple.mpegurl") + output.println("Content-Length: ${playlist.length}") + output.println("Access-Control-Allow-Origin: *") + output.println("Access-Control-Allow-Methods: GET, POST, OPTIONS") + output.println("Access-Control-Allow-Headers: Content-Type") + output.println("Cache-Control: no-cache") + output.println() + output.print(playlist) + output.flush() + + Logger.step("HLS_PLAYLIST_SENT", "📋 M3U8 playlist sent") + } + + private fun generateM3U8Playlist(): String { + val playlist = StringBuilder() + playlist.append("#EXTM3U\n") + playlist.append("#EXT-X-VERSION:3\n") + playlist.append("#EXT-X-TARGETDURATION:10\n") + playlist.append("#EXT-X-MEDIA-SEQUENCE:$segmentCounter\n") + playlist.append("#EXT-X-PLAYLIST-TYPE:EVENT\n") + + // Добавляем последние сегменты + val segments = segmentQueue.toList().takeLast(5) + segments.forEach { segment -> + playlist.append("#EXTINF:10.0,\n") + playlist.append("$segment\n") + } + + return playlist.toString() + } + + private fun sendSegment(output: PrintWriter, segmentNumber: Int) { + // В реальной реализации здесь будет отправка H.264/MPEG-TS сегмента + val segmentData = generateDummySegment(segmentNumber) + + output.println("HTTP/1.1 200 OK") + output.println("Content-Type: video/mp2t") + output.println("Content-Length: ${segmentData.size}") + output.println("Access-Control-Allow-Origin: *") + output.println() + output.flush() + + // Отправляем бинарные данные через OutputStream сокета + val clientSocket = (output as? PrintWriter)?.let { + // Получаем сокет из контекста (нужно передать его в метод) + null // Временное решение + } + + Logger.step("HLS_SEGMENT_SENT", "🎥 Segment $segmentNumber sent") + } + + private fun generateDummySegment(segmentNumber: Int): ByteArray { + // Заглушка - в реальной реализации здесь будут закодированные кадры + return "DUMMY_TS_SEGMENT_$segmentNumber".toByteArray() + } + + private fun extractSegmentNumber(requestLine: String): Int { + return try { + val regex = "segment(\\d+)\\.ts".toRegex() + val match = regex.find(requestLine) + match?.groupValues?.get(1)?.toInt() ?: 0 + } catch (_: Exception) { + 0 + } + } + + private fun sendCORSHeaders(output: PrintWriter) { + output.println("HTTP/1.1 200 OK") + output.println("Access-Control-Allow-Origin: *") + output.println("Access-Control-Allow-Methods: GET, POST, OPTIONS") + output.println("Access-Control-Allow-Headers: Content-Type") + output.println("Content-Length: 0") + output.println() + output.flush() + } + + private fun send404(output: PrintWriter) { + output.println("HTTP/1.1 404 Not Found") + output.println("Content-Length: 0") + output.println() + output.flush() + } + + private fun startSegmentGeneration() { + Thread { + Logger.step("HLS_SEGMENT_GENERATION", "🎬 Starting HLS segment generation") + + while (isServerRunning.get()) { + try { + // Генерируем новый сегмент каждые 10 секунд + val segmentName = "segment${segmentCounter++}.ts" + segmentQueue.offer(segmentName) + + // Ограничиваем количество сегментов + while (segmentQueue.size > 10) { + segmentQueue.poll() + } + + Logger.step("HLS_SEGMENT_GENERATED", "📹 Generated segment: $segmentName") + Thread.sleep(10000) // 10 секунд на сегмент + + } catch (_: InterruptedException) { + break + } catch (e: Exception) { + Logger.error("HLS_SEGMENT_ERROR", "Error generating segment", e) + } + } + }.start() + } + + fun switchCamera(cameraType: String) { + Logger.step("HLS_SWITCH_CAMERA", "🔄 Switching HLS camera to: $cameraType") + // В реальной реализации здесь будет переключение источника кадров + // TODO: Implement camera switching logic + } + + fun stopStreaming() { + Logger.step("HLS_STOP_STREAMING", "🛑 Stopping HLS streaming") + + try { + isServerRunning.set(false) + httpServer?.close() + httpServer = null + + serverThread?.interrupt() + serverThread = null + + segmentQueue.clear() + segmentCounter = 0 + + Logger.step("HLS_STREAMING_STOPPED", "✅ HLS streaming stopped") + + } catch (e: Exception) { + Logger.error("HLS_STOP_ERROR", "Error stopping HLS streaming", e) + } + } + + fun dispose() { + stopStreaming() + } +} diff --git a/app/src/main/java/com/example/godeye/streaming/RTSPStreamManager.kt b/app/src/main/java/com/example/godeye/streaming/RTSPStreamManager.kt new file mode 100644 index 0000000..ee533df --- /dev/null +++ b/app/src/main/java/com/example/godeye/streaming/RTSPStreamManager.kt @@ -0,0 +1,331 @@ +package com.example.godeye.streaming + +import android.content.Context +import android.hardware.camera2.* +import android.media.MediaRecorder +import android.os.Handler +import android.os.HandlerThread +import com.example.godeye.utils.Logger +import java.io.* +import java.net.* +import java.util.concurrent.atomic.AtomicBoolean + +/** + * RTSP Stream Manager - создает RTSP сервер на Android устройстве + * Позволяет операторам подключаться напрямую через rtsp://device_ip:8554/live + */ +class RTSPStreamManager(private val context: Context) { + + private var serverSocket: ServerSocket? = null + private var isServerRunning = AtomicBoolean(false) + private var serverThread: Thread? = null + private val clientSockets = mutableListOf() + + private var cameraDevice: CameraDevice? = null + private var captureSession: CameraCaptureSession? = null + private var backgroundThread: HandlerThread? = null + private var backgroundHandler: Handler? = null + + private val serverPort = 8554 + private var deviceIP: String? = null + + init { + detectDeviceIP() + startBackgroundThread() + } + + private fun detectDeviceIP() { + try { + val interfaces = NetworkInterface.getNetworkInterfaces() + while (interfaces.hasMoreElements()) { + val networkInterface = interfaces.nextElement() + if (!networkInterface.isLoopback && networkInterface.isUp) { + val addresses = networkInterface.inetAddresses + while (addresses.hasMoreElements()) { + val address = addresses.nextElement() + if (!address.isLoopbackAddress && address.isSiteLocalAddress) { + deviceIP = address.hostAddress + break + } + } + } + } + } catch (e: Exception) { + Logger.error("RTSP_IP_DETECTION", "Failed to detect IP", e) + } + } + + private fun startBackgroundThread() { + backgroundThread = HandlerThread("CameraBackground").also { it.start() } + backgroundHandler = Handler(backgroundThread?.looper!!) + } + + fun startServer(cameraType: String = "back"): String? { + Logger.step("RTSP_START_SERVER", "🎬 Starting RTSP server on port $serverPort") + + try { + if (isServerRunning.get()) { + Logger.step("RTSP_ALREADY_RUNNING", "RTSP server already running") + return "rtsp://$deviceIP:$serverPort/live" + } + + serverSocket = ServerSocket(serverPort) + isServerRunning.set(true) + + serverThread = Thread { + while (isServerRunning.get()) { + try { + val clientSocket = serverSocket?.accept() + if (clientSocket != null) { + clientSockets.add(clientSocket) + handleRTSPClient(clientSocket) + } + } catch (e: Exception) { + if (isServerRunning.get()) { + Logger.error("RTSP_CLIENT_ERROR", "Error handling client", e) + } + } + } + } + serverThread?.start() + + initializeCamera(cameraType) + + val rtspUrl = "rtsp://$deviceIP:$serverPort/live" + Logger.step("RTSP_SERVER_STARTED", "✅ RTSP server started: $rtspUrl") + return rtspUrl + + } catch (e: Exception) { + Logger.error("RTSP_START_ERROR", "Failed to start RTSP server", e) + return null + } + } + + private fun handleRTSPClient(clientSocket: Socket) { + Thread { + try { + val input = BufferedReader(InputStreamReader(clientSocket.getInputStream())) + val output = PrintWriter(clientSocket.getOutputStream(), true) + + var line: String? + val request = StringBuilder() + + // Читаем RTSP запрос + while (input.readLine().also { line = it } != null) { + request.append(line).append("\n") + if (line!!.isEmpty()) break + } + + val requestStr = request.toString() + Logger.step("RTSP_REQUEST", "📡 RTSP request: ${requestStr.lines().firstOrNull()}") + + when { + requestStr.contains("OPTIONS") -> { + sendRTSPResponse(output, "200 OK", mapOf( + "Public" to "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE" + )) + } + requestStr.contains("DESCRIBE") -> { + val sdp = generateSDP() + sendRTSPResponse(output, "200 OK", mapOf( + "Content-Type" to "application/sdp", + "Content-Length" to sdp.length.toString() + ), sdp) + } + requestStr.contains("SETUP") -> { + sendRTSPResponse(output, "200 OK", mapOf( + "Transport" to "RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001", + "Session" to "12345678" + )) + } + requestStr.contains("PLAY") -> { + sendRTSPResponse(output, "200 OK", mapOf( + "Session" to "12345678" + )) + startRTPStreaming(clientSocket) + } + } + + } catch (e: Exception) { + Logger.error("RTSP_CLIENT_HANDLER", "Error handling RTSP client", e) + } finally { + clientSocket.close() + clientSockets.remove(clientSocket) + } + }.start() + } + + private fun sendRTSPResponse(output: PrintWriter, status: String, headers: Map, body: String = "") { + output.println("RTSP/1.0 $status") + output.println("CSeq: 1") + headers.forEach { (key, value) -> + output.println("$key: $value") + } + output.println() + if (body.isNotEmpty()) { + output.print(body) + } + output.flush() + } + + private fun generateSDP(): String { + return """v=0 +o=- 0 0 IN IP4 $deviceIP +s=Android Camera Stream +c=IN IP4 $deviceIP +t=0 0 +m=video 9000 RTP/AVP 96 +a=rtpmap:96 H264/90000 +a=fmtp:96 profile-level-id=42e01e +a=control:streamid=0 +""" + } + + private fun startRTPStreaming(clientSocket: Socket) { + Logger.step("RTSP_START_RTP", "🎥 Starting RTP streaming to client") + + Thread { + try { + val rtpSocket = DatagramSocket(9000) + val clientAddress = clientSocket.inetAddress + + // Симуляция RTP пакетов (в реальной реализации здесь будут кадры с камеры) + var sequenceNumber = 0 + val timestamp = System.currentTimeMillis() + + while (isServerRunning.get() && !clientSocket.isClosed) { + // Создаем простой RTP пакет + val rtpPacket = createRTPPacket(sequenceNumber++, timestamp, "dummy_frame".toByteArray()) + val packet = DatagramPacket(rtpPacket, rtpPacket.size, clientAddress, 8000) + rtpSocket.send(packet) + + Thread.sleep(33) // ~30 FPS + } + + rtpSocket.close() + + } catch (e: Exception) { + Logger.error("RTSP_RTP_ERROR", "Error in RTP streaming", e) + } + }.start() + } + + private fun createRTPPacket(sequenceNumber: Int, timestamp: Long, payload: ByteArray): ByteArray { + val header = ByteArray(12) + + // RTP Header + header[0] = 0x80.toByte() // Version 2, no padding, no extension, no CC + header[1] = 0x60.toByte() // Marker bit + Payload type (96) + + // Sequence number + header[2] = (sequenceNumber shr 8).toByte() + header[3] = (sequenceNumber and 0xFF).toByte() + + // Timestamp + val ts = (timestamp and 0xFFFFFFFF).toInt() + header[4] = (ts shr 24).toByte() + header[5] = (ts shr 16).toByte() + header[6] = (ts shr 8).toByte() + header[7] = (ts and 0xFF).toByte() + + // SSRC (synchronization source identifier) + header[8] = 0x12.toByte() + header[9] = 0x34.toByte() + header[10] = 0x56.toByte() + header[11] = 0x78.toByte() + + return header + payload + } + + private fun initializeCamera(cameraType: String) { + Logger.step("RTSP_INIT_CAMERA", "📷 Initializing camera for RTSP") + + try { + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + val cameraId = if (cameraType == "front") { + cameraManager.cameraIdList.find { + val characteristics = cameraManager.getCameraCharacteristics(it) + characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT + } + } else { + cameraManager.cameraIdList.find { + val characteristics = cameraManager.getCameraCharacteristics(it) + characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK + } + } ?: cameraManager.cameraIdList.firstOrNull() + + if (cameraId != null) { + cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { + override fun onOpened(camera: CameraDevice) { + cameraDevice = camera + Logger.step("RTSP_CAMERA_OPENED", "✅ Camera opened for RTSP") + createCaptureSession() + } + + override fun onDisconnected(camera: CameraDevice) { + camera.close() + cameraDevice = null + } + + override fun onError(camera: CameraDevice, error: Int) { + Logger.error("RTSP_CAMERA_ERROR", "Camera error: $error") + camera.close() + cameraDevice = null + } + }, backgroundHandler) + } + + } catch (e: Exception) { + Logger.error("RTSP_CAMERA_INIT_ERROR", "Failed to initialize camera", e) + } + } + + private fun createCaptureSession() { + // В реальной реализации здесь будет создание сессии захвата кадров + // и их кодирование в H.264 для передачи через RTP + Logger.step("RTSP_CAPTURE_SESSION", "📹 Camera capture session created") + } + + fun switchCamera(cameraType: String) { + Logger.step("RTSP_SWITCH_CAMERA", "🔄 Switching RTSP camera to: $cameraType") + + // Закрываем текущую камеру и открываем новую + cameraDevice?.close() + initializeCamera(cameraType) + } + + fun stopServer() { + Logger.step("RTSP_STOP_SERVER", "🛑 Stopping RTSP server") + + try { + isServerRunning.set(false) + + clientSockets.forEach { it.close() } + clientSockets.clear() + + serverSocket?.close() + serverSocket = null + + cameraDevice?.close() + cameraDevice = null + + captureSession?.close() + captureSession = null + + serverThread?.interrupt() + serverThread = null + + Logger.step("RTSP_SERVER_STOPPED", "✅ RTSP server stopped") + + } catch (e: Exception) { + Logger.error("RTSP_STOP_ERROR", "Error stopping RTSP server", e) + } + } + + fun dispose() { + stopServer() + backgroundThread?.quitSafely() + backgroundThread = null + backgroundHandler = null + } +} diff --git a/app/src/main/java/com/example/godeye/streaming/UDPStreamManager.kt b/app/src/main/java/com/example/godeye/streaming/UDPStreamManager.kt new file mode 100644 index 0000000..481654f --- /dev/null +++ b/app/src/main/java/com/example/godeye/streaming/UDPStreamManager.kt @@ -0,0 +1,193 @@ +package com.example.godeye.streaming + +import android.content.Context +import com.example.godeye.utils.Logger +import java.net.* +import java.util.concurrent.atomic.AtomicBoolean + +/** + * UDP Stream Manager - прямая передача видео через UDP для минимальной задержки + * Позволяет операторам получать сырой видео поток через udp://device_ip:9999 + */ +class UDPStreamManager(private val context: Context) { + + private var udpSocket: DatagramSocket? = null + private var isStreaming = AtomicBoolean(false) + private var streamingThread: Thread? = null + + private val streamingPort = 9999 + private var deviceIP: String? = null + private var targetAddress: InetAddress? = null + private var targetPort: Int = 0 + + init { + detectDeviceIP() + } + + private fun detectDeviceIP() { + try { + val interfaces = NetworkInterface.getNetworkInterfaces() + while (interfaces.hasMoreElements()) { + val networkInterface = interfaces.nextElement() + if (!networkInterface.isLoopback && networkInterface.isUp) { + val addresses = networkInterface.inetAddresses + while (addresses.hasMoreElements()) { + val address = addresses.nextElement() + if (!address.isLoopbackAddress && address.isSiteLocalAddress) { + deviceIP = address.hostAddress + break + } + } + } + } + } catch (e: Exception) { + Logger.error("UDP_IP_DETECTION", "Failed to detect IP", e) + } + } + + fun startStreaming(cameraType: String = "back"): String? { + Logger.step("UDP_START_STREAMING", "🎬 Starting UDP streaming on port $streamingPort") + + try { + if (isStreaming.get()) { + Logger.step("UDP_ALREADY_STREAMING", "UDP streaming already active") + return "udp://$deviceIP:$streamingPort" + } + + udpSocket = DatagramSocket(streamingPort) + isStreaming.set(true) + + startFrameStreaming(cameraType) + + val udpUrl = "udp://$deviceIP:$streamingPort" + Logger.step("UDP_STREAMING_STARTED", "✅ UDP streaming started: $udpUrl") + return udpUrl + + } catch (e: Exception) { + Logger.error("UDP_START_ERROR", "Failed to start UDP streaming", e) + return null + } + } + + private fun startFrameStreaming(cameraType: String) { + streamingThread = Thread { + Logger.step("UDP_FRAME_STREAMING", "🎥 Starting UDP frame streaming") + + var frameCounter = 0 + + while (isStreaming.get()) { + try { + // В реальной реализации здесь будут кадры с камеры + val frameData = generateDummyFrame(frameCounter++, cameraType) + + // Если есть подключенные клиенты, отправляем им кадры + if (targetAddress != null && targetPort > 0) { + sendFrame(frameData) + } + + // ~30 FPS + Thread.sleep(33) + + } catch (e: InterruptedException) { + break + } catch (e: Exception) { + Logger.error("UDP_FRAME_ERROR", "Error streaming frame", e) + } + } + } + streamingThread?.start() + } + + private fun generateDummyFrame(frameNumber: Int, cameraType: String): ByteArray { + // Заглушка - в реальной реализации здесь будут сжатые кадры H.264 + val frameHeader = ByteArray(8) + + // Frame header: magic number + frame number + camera type + frameHeader[0] = 0x47.toByte() // Magic 'G' + frameHeader[1] = 0x45.toByte() // Magic 'E' + frameHeader[2] = (frameNumber shr 24).toByte() + frameHeader[3] = (frameNumber shr 16).toByte() + frameHeader[4] = (frameNumber shr 8).toByte() + frameHeader[5] = (frameNumber and 0xFF).toByte() + frameHeader[6] = if (cameraType == "back") 0x00 else 0x01 + frameHeader[7] = 0x00 // Reserved + + val frameData = "FRAME_${frameNumber}_${cameraType}_${System.currentTimeMillis()}".toByteArray() + return frameHeader + frameData + } + + private fun sendFrame(frameData: ByteArray) { + try { + val packet = DatagramPacket( + frameData, + frameData.size, + targetAddress, + targetPort + ) + udpSocket?.send(packet) + + } catch (e: Exception) { + Logger.error("UDP_SEND_FRAME", "Error sending UDP frame", e) + } + } + + /** + * Устанавливает адрес клиента для отправки кадров + */ + fun setClient(clientIP: String, clientPort: Int) { + try { + targetAddress = InetAddress.getByName(clientIP) + targetPort = clientPort + Logger.step("UDP_CLIENT_SET", "📡 UDP client set: $clientIP:$clientPort") + } catch (e: Exception) { + Logger.error("UDP_SET_CLIENT", "Error setting UDP client", e) + } + } + + /** + * Получение информации о UDP стриме для отправки клиенту + */ + fun getStreamInfo(): Map { + return mapOf( + "protocol" to "udp", + "ip" to (deviceIP ?: "unknown"), + "port" to streamingPort, + "url" to "udp://$deviceIP:$streamingPort", + "format" to "raw_frames", + "fps" to 30, + "active" to isStreaming.get() + ) + } + + fun switchCamera(cameraType: String) { + Logger.step("UDP_SWITCH_CAMERA", "🔄 Switching UDP camera to: $cameraType") + // В реальной реализации здесь будет переключение источника кадров + // Новый тип камеры будет включен в следующие кадры + } + + fun stopStreaming() { + Logger.step("UDP_STOP_STREAMING", "🛑 Stopping UDP streaming") + + try { + isStreaming.set(false) + + streamingThread?.interrupt() + streamingThread = null + + udpSocket?.close() + udpSocket = null + + targetAddress = null + targetPort = 0 + + Logger.step("UDP_STREAMING_STOPPED", "✅ UDP streaming stopped") + + } catch (e: Exception) { + Logger.error("UDP_STOP_ERROR", "Error stopping UDP streaming", e) + } + } + + fun dispose() { + stopStreaming() + } +} diff --git a/app/src/main/java/com/example/godeye/streaming/UnifiedStreamingManager.kt b/app/src/main/java/com/example/godeye/streaming/UnifiedStreamingManager.kt new file mode 100644 index 0000000..fbb7dbe --- /dev/null +++ b/app/src/main/java/com/example/godeye/streaming/UnifiedStreamingManager.kt @@ -0,0 +1,355 @@ +package com.example.godeye.streaming + +import android.content.Context +import android.hardware.camera2.CameraManager +import com.example.godeye.utils.Logger +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.json.JSONObject +import java.net.NetworkInterface +import java.net.SocketException + +/** + * Unified Streaming Manager - управляет различными протоколами прямой передачи видео + * + * Поддерживаемые протоколы: + * 1. WebRTC P2P - для веб-браузеров + * 2. RTSP Server - для специализированных клиентов + * 3. HTTP Live Streaming (HLS) - для универсальной совместимости + * 4. Raw UDP Stream - для минимальной задержки + */ +class UnifiedStreamingManager( + private val context: Context, + private val onSignalingMessage: (message: JSONObject) -> Unit +) { + + // Состояния стриминга + private val _streamingState = MutableStateFlow(StreamingState.STOPPED) + val streamingState: StateFlow = _streamingState.asStateFlow() + + private val _availableProtocols = MutableStateFlow>(emptyList()) + val availableProtocols: StateFlow> = _availableProtocols.asStateFlow() + + private val _activeStreams = MutableStateFlow>(emptyMap()) + val activeStreams: StateFlow> = _activeStreams.asStateFlow() + + // Менеджеры протоколов + private var webRTCManager: WebRTCStreamManager? = null + private var rtspManager: RTSPStreamManager? = null + private var hlsManager: HLSStreamManager? = null + private var udpManager: UDPStreamManager? = null + + private var deviceIP: String? = null + + init { + Logger.step("UNIFIED_STREAMING_INIT", "🎬 Initializing Unified Streaming Manager") + detectDeviceIP() + initializeProtocolSupport() + } + + /** + * Определение IP адреса устройства для прямых соединений + */ + private fun detectDeviceIP() { + try { + val interfaces = NetworkInterface.getNetworkInterfaces() + while (interfaces.hasMoreElements()) { + val networkInterface = interfaces.nextElement() + if (!networkInterface.isLoopback && networkInterface.isUp) { + val addresses = networkInterface.inetAddresses + while (addresses.hasMoreElements()) { + val address = addresses.nextElement() + if (!address.isLoopbackAddress && address.isSiteLocalAddress) { + deviceIP = address.hostAddress + Logger.step("DEVICE_IP_DETECTED", "📍 Device IP: $deviceIP") + break + } + } + } + } + } catch (e: SocketException) { + Logger.error("IP_DETECTION_ERROR", "Failed to detect device IP", e) + } + } + + /** + * Инициализация поддержки различных протоколов + */ + private fun initializeProtocolSupport() { + val supportedProtocols = mutableListOf() + + try { + // WebRTC поддержка + webRTCManager = WebRTCStreamManager(context, onSignalingMessage) + supportedProtocols.add( + StreamingProtocol( + type = "webrtc", + name = "WebRTC P2P", + description = "Прямое P2P соединение для веб-браузеров", + isSupported = true, + connectionInfo = "Автоматическое P2P соединение" + ) + ) + Logger.step("WEBRTC_SUPPORT", "✅ WebRTC protocol supported") + } catch (e: Exception) { + Logger.error("WEBRTC_SUPPORT_ERROR", "WebRTC not supported", e) + } + + try { + // RTSP Server поддержка + rtspManager = RTSPStreamManager(context) + supportedProtocols.add( + StreamingProtocol( + type = "rtsp", + name = "RTSP Server", + description = "RTSP сервер для специализированных клиентов", + isSupported = true, + connectionInfo = "rtsp://$deviceIP:8554/live" + ) + ) + Logger.step("RTSP_SUPPORT", "✅ RTSP protocol supported") + } catch (e: Exception) { + Logger.error("RTSP_SUPPORT_ERROR", "RTSP not supported", e) + } + + try { + // HLS поддержка + hlsManager = HLSStreamManager() + supportedProtocols.add( + StreamingProtocol( + type = "hls", + name = "HTTP Live Streaming", + description = "HLS стрим для универсальной совместимости", + isSupported = true, + connectionInfo = "http://$deviceIP:8080/hls/stream.m3u8" + ) + ) + Logger.step("HLS_SUPPORT", "✅ HLS protocol supported") + } catch (e: Exception) { + Logger.error("HLS_SUPPORT_ERROR", "HLS not supported", e) + } + + try { + // UDP Raw Stream поддержка + udpManager = UDPStreamManager(context) + supportedProtocols.add( + StreamingProtocol( + type = "udp", + name = "Raw UDP Stream", + description = "Прямой UDP поток для минимальной задержки", + isSupported = true, + connectionInfo = "udp://$deviceIP:9999" + ) + ) + Logger.step("UDP_SUPPORT", "✅ UDP protocol supported") + } catch (e: Exception) { + Logger.error("UDP_SUPPORT_ERROR", "UDP not supported", e) + } + + _availableProtocols.value = supportedProtocols + Logger.step("PROTOCOLS_INITIALIZED", "🎯 Initialized ${supportedProtocols.size} streaming protocols") + } + + /** + * Запуск стриминга с выбранными протоколами + */ + fun startStreaming( + sessionId: String, + requestedProtocols: List = listOf("webrtc", "rtsp"), + cameraType: String = "back" + ) { + Logger.step("START_STREAMING", "🎬 Starting streaming for session: $sessionId") + Logger.step("STREAMING_PROTOCOLS", "📡 Requested protocols: ${requestedProtocols.joinToString(", ")}") + + _streamingState.value = StreamingState.STARTING + val activeStreams = mutableMapOf() + + try { + // Запуск WebRTC если запрошен + if ("webrtc" in requestedProtocols && webRTCManager != null) { + webRTCManager?.startStreaming(sessionId, cameraType) + activeStreams["webrtc"] = StreamInfo( + protocol = "webrtc", + sessionId = sessionId, + isActive = true, + connectionUrl = "P2P Connection", + startTime = System.currentTimeMillis() + ) + Logger.step("WEBRTC_STARTED", "✅ WebRTC streaming started") + } + + // Запуск RTSP если запрошен + if ("rtsp" in requestedProtocols && rtspManager != null) { + val rtspUrl = rtspManager?.startServer(cameraType) + if (rtspUrl != null) { + activeStreams["rtsp"] = StreamInfo( + protocol = "rtsp", + sessionId = sessionId, + isActive = true, + connectionUrl = rtspUrl, + startTime = System.currentTimeMillis() + ) + Logger.step("RTSP_STARTED", "✅ RTSP streaming started: $rtspUrl") + } + } + + // Запуск HLS если запрошен + if ("hls" in requestedProtocols && hlsManager != null) { + val hlsUrl = hlsManager?.startStreaming(cameraType) + if (hlsUrl != null) { + activeStreams["hls"] = StreamInfo( + protocol = "hls", + sessionId = sessionId, + isActive = true, + connectionUrl = hlsUrl, + startTime = System.currentTimeMillis() + ) + Logger.step("HLS_STARTED", "✅ HLS streaming started: $hlsUrl") + } + } + + // Запуск UDP если запрошен + if ("udp" in requestedProtocols && udpManager != null) { + val udpUrl = udpManager?.startStreaming(cameraType) + if (udpUrl != null) { + activeStreams["udp"] = StreamInfo( + protocol = "udp", + sessionId = sessionId, + isActive = true, + connectionUrl = udpUrl, + startTime = System.currentTimeMillis() + ) + Logger.step("UDP_STARTED", "✅ UDP streaming started: $udpUrl") + } + } + + _activeStreams.value = activeStreams + _streamingState.value = if (activeStreams.isNotEmpty()) StreamingState.ACTIVE else StreamingState.ERROR + + // Отправляем информацию о доступных стримах оператору через сигнальный сервер + sendStreamingInfo(sessionId, activeStreams) + + } catch (e: Exception) { + Logger.error("START_STREAMING_ERROR", "Failed to start streaming", e) + _streamingState.value = StreamingState.ERROR + } + } + + /** + * Остановка всех стримов + */ + fun stopStreaming(sessionId: String) { + Logger.step("STOP_STREAMING", "🛑 Stopping streaming for session: $sessionId") + + try { + webRTCManager?.stopStreaming() + rtspManager?.stopServer() + hlsManager?.stopStreaming() + udpManager?.stopStreaming() + + _activeStreams.value = emptyMap() + _streamingState.value = StreamingState.STOPPED + + Logger.step("STREAMING_STOPPED", "✅ All streaming stopped") + + } catch (e: Exception) { + Logger.error("STOP_STREAMING_ERROR", "Error stopping streaming", e) + } + } + + /** + * Переключение камеры во всех активных стримах + */ + fun switchCamera(cameraType: String) { + Logger.step("SWITCH_CAMERA", "🔄 Switching camera to: $cameraType") + + try { + webRTCManager?.switchCamera(cameraType) + rtspManager?.switchCamera(cameraType) + hlsManager?.switchCamera(cameraType) + udpManager?.switchCamera(cameraType) + + Logger.step("CAMERA_SWITCHED", "✅ Camera switched to: $cameraType") + + } catch (e: Exception) { + Logger.error("SWITCH_CAMERA_ERROR", "Error switching camera", e) + } + } + + /** + * Отправка информации о доступных стримах оператору + */ + private fun sendStreamingInfo(sessionId: String, streams: Map) { + val streamingInfo = JSONObject().apply { + put("type", "streaming_info") + put("sessionId", sessionId) + put("deviceIP", deviceIP) + put("streams", JSONObject().apply { + streams.forEach { (protocol, info) -> + put(protocol, JSONObject().apply { + put("url", info.connectionUrl) + put("active", info.isActive) + put("protocol", info.protocol) + }) + } + }) + } + + onSignalingMessage(streamingInfo) + Logger.step("STREAMING_INFO_SENT", "📡 Streaming info sent to operator") + } + + /** + * Получение статистики стриминга + */ + fun getStreamingStats(): Map { + val stats = mutableMapOf() + + stats["state"] = _streamingState.value.name + stats["activeStreams"] = _activeStreams.value.size + stats["deviceIP"] = deviceIP ?: "unknown" + stats["supportedProtocols"] = _availableProtocols.value.map { it.type } + + _activeStreams.value.forEach { (protocol, info) -> + stats["${protocol}_uptime"] = (System.currentTimeMillis() - info.startTime) / 1000 + } + + return stats + } + + fun dispose() { + Logger.step("UNIFIED_STREAMING_DISPOSE", "🧹 Disposing Unified Streaming Manager") + + try { + stopStreaming("dispose") + webRTCManager?.dispose() + rtspManager?.dispose() + hlsManager?.dispose() + udpManager?.dispose() + } catch (e: Exception) { + Logger.error("DISPOSE_ERROR", "Error during disposal", e) + } + } +} + +// Данные классы для стриминга +enum class StreamingState { + STOPPED, STARTING, ACTIVE, ERROR +} + +data class StreamingProtocol( + val type: String, + val name: String, + val description: String, + val isSupported: Boolean, + val connectionInfo: String +) + +data class StreamInfo( + val protocol: String, + val sessionId: String, + val isActive: Boolean, + val connectionUrl: String, + val startTime: Long +) diff --git a/app/src/main/java/com/example/godeye/streaming/WebRTCStreamManager.kt b/app/src/main/java/com/example/godeye/streaming/WebRTCStreamManager.kt new file mode 100644 index 0000000..7c0cdc7 --- /dev/null +++ b/app/src/main/java/com/example/godeye/streaming/WebRTCStreamManager.kt @@ -0,0 +1,218 @@ +package com.example.godeye.streaming + +import android.content.Context +import com.example.godeye.utils.Logger +import org.json.JSONObject +import org.webrtc.* +import org.webrtc.audio.JavaAudioDeviceModule +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * WebRTC Stream Manager - улучшенная версия для прямого P2P соединения + */ +class WebRTCStreamManager( + private val context: Context, + private val onSignalingMessage: (message: JSONObject) -> Unit +) { + + private var peerConnectionFactory: PeerConnectionFactory? = null + private var peerConnection: PeerConnection? = null + private var localVideoTrack: VideoTrack? = null + private var localAudioTrack: AudioTrack? = null + private var videoCapturer: CameraVideoCapturer? = null + private var surfaceTextureHelper: SurfaceTextureHelper? = null + + private val _connectionState = MutableStateFlow(PeerConnection.PeerConnectionState.NEW) + val connectionState: StateFlow = _connectionState.asStateFlow() + + private val _isStreaming = MutableStateFlow(false) + val isStreaming: StateFlow = _isStreaming.asStateFlow() + + private val iceServers = listOf( + PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(), + PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer() + ) + + init { + initializePeerConnectionFactory() + } + + private fun initializePeerConnectionFactory() { + val initOptions = PeerConnectionFactory.InitializationOptions.builder(context) + .setEnableInternalTracer(true) + .createInitializationOptions() + PeerConnectionFactory.initialize(initOptions) + + val audioDeviceModule = JavaAudioDeviceModule.builder(context).createAudioDeviceModule() + + peerConnectionFactory = PeerConnectionFactory.builder() + .setAudioDeviceModule(audioDeviceModule) + .setVideoEncoderFactory(DefaultVideoEncoderFactory( + EglBase.create().eglBaseContext, true, true)) + .setVideoDecoderFactory(DefaultVideoDecoderFactory(EglBase.create().eglBaseContext)) + .createPeerConnectionFactory() + + Logger.step("WEBRTC_FACTORY_READY", "✅ WebRTC PeerConnectionFactory initialized") + } + + fun startStreaming(sessionId: String, cameraType: String = "back") { + Logger.step("WEBRTC_START_STREAMING", "🎬 Starting WebRTC streaming for session: $sessionId") + + try { + createPeerConnection() + initializeLocalMedia(cameraType) + createOffer(sessionId) + _isStreaming.value = true + + } catch (e: Exception) { + Logger.error("WEBRTC_START_ERROR", "Failed to start WebRTC streaming", e) + } + } + + fun stopStreaming() { + Logger.step("WEBRTC_STOP_STREAMING", "🛑 Stopping WebRTC streaming") + + try { + videoCapturer?.stopCapture() + videoCapturer?.dispose() + + localVideoTrack?.dispose() + localAudioTrack?.dispose() + + peerConnection?.close() + peerConnection = null + + _isStreaming.value = false + _connectionState.value = PeerConnection.PeerConnectionState.CLOSED + + } catch (e: Exception) { + Logger.error("WEBRTC_STOP_ERROR", "Error stopping WebRTC", e) + } + } + + fun switchCamera(cameraType: String) { + Logger.step("WEBRTC_SWITCH_CAMERA", "🔄 Switching WebRTC camera to: $cameraType") + (videoCapturer as? CameraVideoCapturer)?.switchCamera(null) + } + + private fun createPeerConnection() { + val config = PeerConnection.RTCConfiguration(iceServers).apply { + bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE + rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE + } + + peerConnection = peerConnectionFactory?.createPeerConnection(config, object : PeerConnection.Observer { + override fun onSignalingChange(state: PeerConnection.SignalingState) { + Logger.step("WEBRTC_SIGNALING", "Signaling state: $state") + } + + override fun onIceConnectionChange(state: PeerConnection.IceConnectionState) { + Logger.step("WEBRTC_ICE_STATE", "ICE state: $state") + } + + override fun onConnectionChange(state: PeerConnection.PeerConnectionState) { + _connectionState.value = state + Logger.step("WEBRTC_CONNECTION_STATE", "Connection state: $state") + } + + override fun onIceCandidate(candidate: IceCandidate) { + val candidateMsg = JSONObject().apply { + put("type", "ice-candidate") + put("candidate", candidate.sdp) + put("sdpMLineIndex", candidate.sdpMLineIndex) + put("sdpMid", candidate.sdpMid) + } + onSignalingMessage(candidateMsg) + } + + override fun onIceCandidatesRemoved(candidates: Array) {} + override fun onAddStream(stream: MediaStream) {} + override fun onRemoveStream(stream: MediaStream) {} + override fun onDataChannel(channel: DataChannel) {} + override fun onRenegotiationNeeded() {} + override fun onIceGatheringChange(state: PeerConnection.IceGatheringState) {} + override fun onIceConnectionReceivingChange(receiving: Boolean) {} + }) + } + + private fun initializeLocalMedia(cameraType: String) { + val videoSource = peerConnectionFactory?.createVideoSource(false) + localVideoTrack = peerConnectionFactory?.createVideoTrack("video", videoSource) + + val audioSource = peerConnectionFactory?.createAudioSource(MediaConstraints()) + localAudioTrack = peerConnectionFactory?.createAudioTrack("audio", audioSource) + + val stream = peerConnectionFactory?.createLocalMediaStream("stream") + localVideoTrack?.let { stream?.addTrack(it) } + localAudioTrack?.let { stream?.addTrack(it) } + + stream?.let { peerConnection?.addStream(it) } + + initializeCamera(videoSource, cameraType) + } + + private fun initializeCamera(videoSource: VideoSource?, cameraType: String) { + val cameraEnumerator = Camera2Enumerator(context) + val cameraName = if (cameraType == "front") { + cameraEnumerator.deviceNames.find { cameraEnumerator.isFrontFacing(it) } + } else { + cameraEnumerator.deviceNames.find { cameraEnumerator.isBackFacing(it) } + } ?: cameraEnumerator.deviceNames.firstOrNull() + + if (cameraName != null) { + surfaceTextureHelper = SurfaceTextureHelper.create("CameraThread", EglBase.create().eglBaseContext) + videoCapturer = cameraEnumerator.createCapturer(cameraName, null) as? CameraVideoCapturer + + videoCapturer?.initialize(surfaceTextureHelper, context, videoSource?.capturerObserver) + videoCapturer?.startCapture(1280, 720, 30) + } + } + + private fun createOffer(sessionId: String) { + val constraints = MediaConstraints() + peerConnection?.createOffer(object : SdpObserver { + override fun onCreateSuccess(desc: SessionDescription) { + peerConnection?.setLocalDescription(object : SdpObserver { + override fun onSetSuccess() { + val offerMsg = JSONObject().apply { + put("type", "offer") + put("sessionId", sessionId) + put("sdp", desc.description) + } + onSignalingMessage(offerMsg) + } + override fun onSetFailure(error: String) {} + override fun onCreateSuccess(p0: SessionDescription?) {} + override fun onCreateFailure(p0: String?) {} + }, desc) + } + override fun onCreateFailure(error: String) {} + override fun onSetSuccess() {} + override fun onSetFailure(error: String) {} + }, constraints) + } + + fun handleAnswer(answerSdp: String) { + val desc = SessionDescription(SessionDescription.Type.ANSWER, answerSdp) + peerConnection?.setRemoteDescription(object : SdpObserver { + override fun onSetSuccess() { + Logger.step("WEBRTC_ANSWER_SET", "✅ WebRTC answer processed") + } + override fun onSetFailure(error: String) {} + override fun onCreateSuccess(p0: SessionDescription?) {} + override fun onCreateFailure(p0: String?) {} + }, desc) + } + + fun handleIceCandidate(candidate: String, sdpMLineIndex: Int, sdpMid: String) { + val iceCandidate = IceCandidate(sdpMid, sdpMLineIndex, candidate) + peerConnection?.addIceCandidate(iceCandidate) + } + + fun dispose() { + stopStreaming() + peerConnectionFactory?.dispose() + } +} diff --git a/app/src/main/java/com/example/godeye/ui/components/AnimatedComponents.kt b/app/src/main/java/com/example/godeye/ui/components/AnimatedComponents.kt new file mode 100644 index 0000000..d1aef4b --- /dev/null +++ b/app/src/main/java/com/example/godeye/ui/components/AnimatedComponents.kt @@ -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 +} diff --git a/app/src/main/java/com/example/godeye/ui/components/CameraRequestDialog.kt b/app/src/main/java/com/example/godeye/ui/components/CameraRequestDialog.kt deleted file mode 100644 index 3d3ab11..0000000 --- a/app/src/main/java/com/example/godeye/ui/components/CameraRequestDialog.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/java/com/example/godeye/ui/components/ConnectionStatusCard.kt b/app/src/main/java/com/example/godeye/ui/components/ConnectionStatusCard.kt deleted file mode 100644 index c4147ca..0000000 --- a/app/src/main/java/com/example/godeye/ui/components/ConnectionStatusCard.kt +++ /dev/null @@ -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 { - 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) - ) - } -} diff --git a/app/src/main/java/com/example/godeye/ui/components/MainScreenComponents.kt b/app/src/main/java/com/example/godeye/ui/components/MainScreenComponents.kt new file mode 100644 index 0000000..7f89f05 --- /dev/null +++ b/app/src/main/java/com/example/godeye/ui/components/MainScreenComponents.kt @@ -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, + 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, + 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("Запустить") + } + } + } + } + } +} diff --git a/app/src/main/java/com/example/godeye/ui/components/SessionsList.kt b/app/src/main/java/com/example/godeye/ui/components/SessionsList.kt deleted file mode 100644 index 6b0a0f9..0000000 --- a/app/src/main/java/com/example/godeye/ui/components/SessionsList.kt +++ /dev/null @@ -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, - 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 - } -} diff --git a/app/src/main/java/com/example/godeye/ui/components/SettingsScreen.kt b/app/src/main/java/com/example/godeye/ui/components/SettingsScreen.kt new file mode 100644 index 0000000..c0f64f9 --- /dev/null +++ b/app/src/main/java/com/example/godeye/ui/components/SettingsScreen.kt @@ -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 + ) + } + } + } +} diff --git a/app/src/main/java/com/example/godeye/ui/dialogs/CameraRequestDialog.kt b/app/src/main/java/com/example/godeye/ui/dialogs/CameraRequestDialog.kt new file mode 100644 index 0000000..d0c02a9 --- /dev/null +++ b/app/src/main/java/com/example/godeye/ui/dialogs/CameraRequestDialog.kt @@ -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)" + } +} diff --git a/app/src/main/java/com/example/godeye/ui/screens/MainScreen.kt b/app/src/main/java/com/example/godeye/ui/screens/MainScreen.kt deleted file mode 100644 index e872d7f..0000000 --- a/app/src/main/java/com/example/godeye/ui/screens/MainScreen.kt +++ /dev/null @@ -1,318 +0,0 @@ -package com.example.godeye.ui.screens - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Settings -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.input.KeyboardType -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.godeye.R -import com.example.godeye.models.ConnectionState -import com.example.godeye.models.MainScreenState -import com.example.godeye.ui.components.CameraRequestDialog -import com.example.godeye.ui.components.ConnectionStatusCard -import com.example.godeye.ui.components.SessionsList -import com.example.godeye.ui.viewmodels.MainViewModel -import com.example.godeye.ui.viewmodels.UiEvent -import com.example.godeye.utils.collectAsEffect - -/** - * Главный экран приложения - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MainScreen( - viewModel: MainViewModel = viewModel(), - onRequestPermissions: () -> Unit, - onShowError: (String) -> Unit -) { - val uiState by viewModel.uiState.collectAsState() - var serverUrl by remember { mutableStateOf("") } - - // Синхронизируем локальное состояние с ViewModel - LaunchedEffect(uiState.serverUrl) { - serverUrl = uiState.serverUrl - } - - // Обработка UI событий - LaunchedEffect(viewModel) { - viewModel.events.collect { event -> - when (event) { - is UiEvent.RequestPermissions -> onRequestPermissions() - is UiEvent.ShowError -> { - // Получаем текст ошибки внутри LaunchedEffect - val errorMessage = when (event.error) { - is com.example.godeye.models.AppError.NetworkError -> "Ошибка сети" - is com.example.godeye.models.AppError.CameraPermissionDenied -> "Нет разрешения на камеру" - is com.example.godeye.models.AppError.AudioPermissionDenied -> "Нет разрешения на микрофон" - is com.example.godeye.models.AppError.CameraNotAvailable -> "Камера недоступна" - is com.example.godeye.models.AppError.WebRTCConnectionFailed -> "Ошибка WebRTC соединения" - is com.example.godeye.models.AppError.SocketError -> "Ошибка WebSocket: ${event.error.message}" - is com.example.godeye.models.AppError.CameraError -> "Ошибка камеры: ${event.error.message}" - is com.example.godeye.models.AppError.UnknownError -> "Неизвестная ошибка" - } - onShowError(errorMessage) - } - is UiEvent.ShowMessage -> onShowError(event.message) - is UiEvent.ShowCameraRequestDialog -> { - // Диалог будет показан через состояние showCameraRequest - } - } - } - } - - Scaffold( - topBar = { - TopAppBar( - title = { - Text( - text = stringResource(R.string.app_name), - fontWeight = FontWeight.Bold - ) - }, - actions = { - IconButton(onClick = { /* TODO: Настройки */ }) { - Icon(Icons.Default.Settings, contentDescription = stringResource(R.string.settings)) - } - } - ) - } - ) { paddingValues -> - Box(modifier = Modifier.fillMaxSize()) { - MainContent( - uiState = uiState, - serverUrl = serverUrl, - onServerUrlChange = { serverUrl = it }, - onConnect = { - viewModel.updateServerUrl(serverUrl) - viewModel.connect() - }, - onDisconnect = viewModel::disconnect, - onEndSession = viewModel::endSession, - modifier = Modifier.padding(paddingValues) - ) - - // Диалог запроса камеры - uiState.showCameraRequest?.let { request -> - CameraRequestDialog( - request = request, - onAccept = { viewModel.respondToCameraRequest(request.sessionId, true) }, - onDeny = { viewModel.respondToCameraRequest(request.sessionId, false) }, - onDismiss = { viewModel.respondToCameraRequest(request.sessionId, false) } - ) - } - - // Индикатор загрузки - if (uiState.isLoading) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Card( - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) - Spacer(modifier = Modifier.width(12.dp)) - Text(stringResource(R.string.loading)) - } - } - } - } - } - } -} - -/** - * Основное содержимое экрана - */ -@Composable -private fun MainContent( - uiState: MainScreenState, - serverUrl: String, - onServerUrlChange: (String) -> Unit, - onConnect: () -> Unit, - onDisconnect: () -> Unit, - onEndSession: (String) -> Unit, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier - .fillMaxSize() - .padding(16.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // Информация об устройстве - DeviceInfoCard(deviceId = uiState.deviceId) - - // Статус подключения - ConnectionStatusCard(connectionState = uiState.connectionState) - - // Настройки подключения - ConnectionSettingsCard( - serverUrl = serverUrl, - onServerUrlChange = onServerUrlChange, - connectionState = uiState.connectionState, - onConnect = onConnect, - onDisconnect = onDisconnect - ) - - // Список активных сессий - ActiveSessionsCard( - sessions = uiState.activeSessions, - onEndSession = onEndSession - ) - } -} - -/** - * Карточка с информацией об устройстве - */ -@Composable -private fun DeviceInfoCard( - deviceId: String, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = stringResource(R.string.device_id_label), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = deviceId.ifEmpty { "..." }, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - } - } -} - -/** - * Карточка настроек подключения - */ -@Composable -private fun ConnectionSettingsCard( - serverUrl: String, - onServerUrlChange: (String) -> Unit, - connectionState: ConnectionState, - onConnect: () -> Unit, - onDisconnect: () -> Unit, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // Поле ввода URL сервера - OutlinedTextField( - value = serverUrl, - onValueChange = onServerUrlChange, - label = { Text(stringResource(R.string.server_url_label)) }, - placeholder = { Text(stringResource(R.string.server_url_hint)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), - enabled = connectionState == ConnectionState.DISCONNECTED, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) - - // Кнопка подключения/отключения - val isConnected = connectionState == ConnectionState.CONNECTED - val isLoading = connectionState == ConnectionState.CONNECTING || - connectionState == ConnectionState.RECONNECTING - - Button( - onClick = if (isConnected) onDisconnect else onConnect, - modifier = Modifier.fillMaxWidth(), - enabled = !isLoading && serverUrl.isNotBlank() - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary - ) - Spacer(modifier = Modifier.width(8.dp)) - } - Text( - if (isConnected) stringResource(R.string.disconnect_button) - else stringResource(R.string.connect_button) - ) - } - } - } -} - -/** - * Карточка активных сессий - */ -@Composable -private fun ActiveSessionsCard( - sessions: List, - onEndSession: (String) -> Unit, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.active_sessions_label), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - if (sessions.isNotEmpty()) { - Badge { - Text("${sessions.size}") - } - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - SessionsList( - sessions = sessions, - onEndSession = onEndSession - ) - } - } -} diff --git a/app/src/main/java/com/example/godeye/ui/theme/Color.kt b/app/src/main/java/com/example/godeye/ui/theme/Color.kt deleted file mode 100644 index 55ca619..0000000 --- a/app/src/main/java/com/example/godeye/ui/theme/Color.kt +++ /dev/null @@ -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) \ No newline at end of file diff --git a/app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt b/app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt new file mode 100644 index 0000000..6ea6862 --- /dev/null +++ b/app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/example/godeye/ui/theme/Theme.kt b/app/src/main/java/com/example/godeye/ui/theme/Theme.kt deleted file mode 100644 index 2fae9d9..0000000 --- a/app/src/main/java/com/example/godeye/ui/theme/Theme.kt +++ /dev/null @@ -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 - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/example/godeye/ui/theme/Type.kt b/app/src/main/java/com/example/godeye/ui/theme/Type.kt deleted file mode 100644 index a2e1fa7..0000000 --- a/app/src/main/java/com/example/godeye/ui/theme/Type.kt +++ /dev/null @@ -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 - ) - */ -) \ No newline at end of file diff --git a/app/src/main/java/com/example/godeye/ui/viewmodels/MainViewModel.kt b/app/src/main/java/com/example/godeye/ui/viewmodels/MainViewModel.kt deleted file mode 100644 index 83a46ee..0000000 --- a/app/src/main/java/com/example/godeye/ui/viewmodels/MainViewModel.kt +++ /dev/null @@ -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() - 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 = _uiState.asStateFlow() - - // События для UI - private val _events = MutableSharedFlow() - val events: SharedFlow = _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 { - 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() -} diff --git a/app/src/main/java/com/example/godeye/utils/Constants.kt b/app/src/main/java/com/example/godeye/utils/Constants.kt index ff58c7a..41dd1e4 100644 --- a/app/src/main/java/com/example/godeye/utils/Constants.kt +++ b/app/src/main/java/com/example/godeye/utils/Constants.kt @@ -1,49 +1,91 @@ package com.example.godeye.utils +/** + * Constants - константы приложения согласно ТЗ + */ object Constants { - // 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" - } - // Типы камер + // Настройки сервера согласно ТЗ + const val DEFAULT_SERVER_URL = "http://192.168.219.108:3001" + const val LOCALHOST_SERVER_URL = "http://localhost:3001" + const val LOCAL_NETWORK_SERVER_URL = "http://192.168.1.100:3001" + + // Настройки 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 { const val BACK = "back" const val FRONT = "front" - const val WIDE = "wide" + const val ULTRA_WIDE = "ultra_wide" 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" + } + + // SharedPreferences ключи согласно ТЗ object PreferenceKeys { const val SERVER_URL = "server_url" const val DEVICE_ID = "device_id" const val AUTO_ACCEPT_REQUESTS = "auto_accept_requests" const val CAMERA_QUALITY = "camera_quality" 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 FOREGROUND_SERVICE_ID = 1001 + const val SERVICE_NOTIFICATION_ID = 1001 + const val CAMERA_REQUEST_NOTIFICATION_ID = 1002 - // WebRTC настройки - val STUN_SERVERS = listOf( - "stun:stun.l.google.com:19302", - "stun:stun1.l.google.com:19302" - ) + // Таймауты + const val CAMERA_OPEN_TIMEOUT = 2500L + const val WEBRTC_CONNECTION_TIMEOUT = 10000L + 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 + } } diff --git a/app/src/main/java/com/example/godeye/utils/ErrorHandler.kt b/app/src/main/java/com/example/godeye/utils/ErrorHandler.kt new file mode 100644 index 0000000..62b7334 --- /dev/null +++ b/app/src/main/java/com/example/godeye/utils/ErrorHandler.kt @@ -0,0 +1,146 @@ +package com.example.godeye.utils + +import android.content.Context +import android.widget.Toast +import androidx.compose.material3.SnackbarHostState +import com.example.godeye.models.AppError +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * ErrorHandler - обработка ошибок согласно ТЗ + * Централизованная обработка всех типов ошибок приложения + */ +class ErrorHandler { + + /** + * Обработка ошибок приложения согласно ТЗ + */ + fun handleError(error: AppError, context: Context, scope: CoroutineScope? = null, snackbarHost: SnackbarHostState? = null) { + Logger.error("APP_ERROR", "Handling application error: ${error::class.simpleName}", null) + + when (error) { + is AppError.NetworkError -> { + showNetworkError(context, scope, snackbarHost) + } + is AppError.CameraPermissionDenied -> { + showPermissionError(context, scope, snackbarHost) + } + is AppError.CameraNotAvailable -> { + showCameraError(context, scope, snackbarHost) + } + is AppError.WebRTCConnectionFailed -> { + showWebRTCError(context, scope, snackbarHost) + } + is AppError.SocketError -> { + showSocketError(context, error.message, scope, snackbarHost) + } + is AppError.UnknownError -> { + showUnknownError(context, error.throwable, scope, snackbarHost) + } + } + } + + /** + * Специальная обработка исключений для предотвращения крашей + */ + fun handleUncaughtException(thread: Thread, throwable: Throwable) { + Logger.error("UNCAUGHT_EXCEPTION", "Uncaught exception in thread: ${thread.name}", throwable) + + // Специальная обработка известных Compose ошибок + when { + throwable.message?.contains("ACTION_HOVER_EXIT event was not cleared") == true -> { + Logger.d("Ignoring Compose hover event bug") + // Игнорируем эту ошибку, так как это известный баг Compose + return + } + throwable.message?.contains("Thread starting during runtime shutdown") == true -> { + Logger.d("Ignoring shutdown thread creation error") + // Игнорируем ошибки создания потоков при завершении + return + } + else -> { + // Для остальных ошибок делаем стандартную обработку + Logger.error("CRITICAL_ERROR", "Critical error occurred", throwable) + } + } + } + + private fun showNetworkError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) { + val message = "Ошибка сети. Проверьте подключение к интернету." + Logger.step("ERROR_NETWORK", message) + + if (scope != null && snackbarHost != null) { + scope.launch { + snackbarHost.showSnackbar(message) + } + } else { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + } + + private fun showPermissionError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) { + val message = "Необходимы разрешения для работы с камерой и микрофоном." + Logger.step("ERROR_PERMISSION", message) + + if (scope != null && snackbarHost != null) { + scope.launch { + snackbarHost.showSnackbar(message) + } + } else { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + } + + private fun showCameraError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) { + val message = "Камера недоступна. Проверьте, что другие приложения не используют камеру." + Logger.step("ERROR_CAMERA", message) + + if (scope != null && snackbarHost != null) { + scope.launch { + snackbarHost.showSnackbar(message) + } + } else { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + } + + private fun showWebRTCError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) { + val message = "Ошибка WebRTC соединения. Попробуйте переподключиться." + Logger.step("ERROR_WEBRTC", message) + + if (scope != null && snackbarHost != null) { + scope.launch { + snackbarHost.showSnackbar(message) + } + } else { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + } + + private fun showSocketError(context: Context, errorMessage: String, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) { + val message = "Ошибка подключения к серверу: $errorMessage" + Logger.step("ERROR_SOCKET", message) + + if (scope != null && snackbarHost != null) { + scope.launch { + snackbarHost.showSnackbar(message) + } + } else { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + } + + private fun showUnknownError(context: Context, throwable: Throwable, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) { + val message = "Неизвестная ошибка: ${throwable.message ?: "Unknown"}" + Logger.error("ERROR_UNKNOWN", message, throwable) + + if (scope != null && snackbarHost != null) { + scope.launch { + snackbarHost.showSnackbar(message) + } + } else { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + } +} diff --git a/app/src/main/java/com/example/godeye/utils/Extensions.kt b/app/src/main/java/com/example/godeye/utils/Extensions.kt index 40efb95..0fdcdf2 100644 --- a/app/src/main/java/com/example/godeye/utils/Extensions.kt +++ b/app/src/main/java/com/example/godeye/utils/Extensions.kt @@ -2,139 +2,12 @@ package com.example.godeye.utils import android.content.Context import android.content.SharedPreferences -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraManager -import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -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.* -/** - * Расширения для Context - */ fun Context.getPreferences(): SharedPreferences { return getSharedPreferences("godeye_prefs", Context.MODE_PRIVATE) } -fun Context.generateDeviceId(): String { - val prefs = getPreferences() - 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 - */ -fun CameraManager.getAvailableCameraTypes(): List { - val cameras = mutableListOf() - try { - for (cameraId in cameraIdList) { - val characteristics = getCameraCharacteristics(cameraId) - val facing = characteristics.get(CameraCharacteristics.LENS_FACING) - - when (facing) { - CameraCharacteristics.LENS_FACING_BACK -> { - // Проверяем на широкоугольный и телеобъектив - val focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) - if (focalLengths != null && focalLengths.isNotEmpty()) { - val minFocalLength = focalLengths.minOrNull() ?: 0f - val maxFocalLength = focalLengths.maxOrNull() ?: 0f - - when { - minFocalLength < 2.8f -> cameras.add(Constants.CameraTypes.WIDE) - maxFocalLength > 5.5f -> cameras.add(Constants.CameraTypes.TELEPHOTO) - else -> cameras.add(Constants.CameraTypes.BACK) - } - } else { - cameras.add(Constants.CameraTypes.BACK) - } - } - 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? { - return try { - for (cameraId in cameraIdList) { - val characteristics = getCameraCharacteristics(cameraId) - val facing = characteristics.get(CameraCharacteristics.LENS_FACING) - - 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) { - Log.e("CameraExtensions", "Error finding camera for type $cameraType", e) - null - } -} - -/** - * Compose расширения для Flow - */ -@Composable -fun Flow.collectAsEffect( - key: Any? = null, - action: suspend (T) -> Unit -) { - val lifecycleOwner = LocalLifecycleOwner.current - LaunchedEffect(key) { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - collect(action) - } - } -} - -/** - * Логирование - */ -object Logger { - private const val TAG = "GodEye" - - fun d(message: String, tag: String = TAG) { - Log.d(tag, message) - } - - fun e(message: String, throwable: Throwable? = null, tag: String = TAG) { - 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) - } +fun generateDeviceId(): String { + return "android_${UUID.randomUUID().toString().take(8)}" } diff --git a/app/src/main/java/com/example/godeye/utils/Logger.kt b/app/src/main/java/com/example/godeye/utils/Logger.kt new file mode 100644 index 0000000..a289253 --- /dev/null +++ b/app/src/main/java/com/example/godeye/utils/Logger.kt @@ -0,0 +1,64 @@ +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() + } + + 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") + } + + fun error(step: String, message: String, throwable: Throwable? = null) { + Log.e(TAG, "💥 ERROR in [$step]: $message", throwable) + println("💥 ERROR in [$step]: $message") + throwable?.printStackTrace() + } +} diff --git a/app/src/main/java/com/example/godeye/webrtc/WebRTCManager.kt b/app/src/main/java/com/example/godeye/webrtc/WebRTCManager.kt new file mode 100644 index 0000000..a04dd54 --- /dev/null +++ b/app/src/main/java/com/example/godeye/webrtc/WebRTCManager.kt @@ -0,0 +1,646 @@ +package com.example.godeye.webrtc + +import android.content.Context +import com.example.godeye.utils.Logger +import org.webrtc.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.json.JSONObject + +/** + * WebRTCManager - обработка WebRTC соединений согласно ТЗ + * Архитектура: Socket.IO для сигнализации, WebRTC для P2P медиа-потоков + */ +class WebRTCManager( + private val context: Context, + private val onSignalingMessage: (message: JSONObject) -> Unit +) { + + private var peerConnectionFactory: PeerConnectionFactory? = null + private val activePeerConnections = mutableMapOf() + private var localVideoTrack: VideoTrack? = null + private var localAudioTrack: AudioTrack? = null + private var videoCapturer: CameraVideoCapturer? = null + + // Состояния соединения + private val _connectionState = MutableStateFlow>(emptyMap()) + val connectionState: StateFlow> = _connectionState.asStateFlow() + + // ICE серверы согласно ТЗ + private val iceServers = listOf( + PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(), + PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer() + ) + + // Конфигурация RTCConfiguration согласно ТЗ + private val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply { + tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED + bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE + rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE + continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY + } + + init { + Logger.step("WEBRTC_INIT", "Initializing WebRTC Manager according to ТЗ") + initializePeerConnectionFactory() + } + + /** + * Инициализация PeerConnectionFactory согласно ТЗ + */ + private fun initializePeerConnectionFactory() { + Logger.step("WEBRTC_FACTORY_INIT", "Initializing PeerConnectionFactory") + + try { + val initializationOptions = PeerConnectionFactory.InitializationOptions.builder(context) + .setEnableInternalTracer(false) + .createInitializationOptions() + PeerConnectionFactory.initialize(initializationOptions) + + val options = PeerConnectionFactory.Options() + peerConnectionFactory = PeerConnectionFactory.builder() + .setOptions(options) + .createPeerConnectionFactory() + + Logger.step("WEBRTC_FACTORY_READY", "PeerConnectionFactory initialized successfully") + + } catch (e: Exception) { + Logger.error("WEBRTC_FACTORY_ERROR", "Failed to initialize PeerConnectionFactory", e) + } + } + + /** + * Начало стриминга для сессии - создание offer согласно ТЗ + */ + fun startStreaming(sessionId: String, cameraType: String) { + Logger.step("WEBRTC_START_STREAMING", "Starting WebRTC streaming for session: $sessionId, camera: $cameraType") + + try { + val peerConnection = createPeerConnection(sessionId) + if (peerConnection == null) { + Logger.error("WEBRTC_START_ERROR", "Failed to create peer connection", null) + return + } + + // Добавление локальных медиа-потоков + addLocalStreams(peerConnection, cameraType) + + // Создание offer согласно ТЗ + createOffer(sessionId, peerConnection) + + } catch (e: Exception) { + Logger.error("WEBRTC_START_ERROR", "Failed to start WebRTC streaming", e) + } + } + + /** + * Создание PeerConnection для сессии + */ + private fun createPeerConnection(sessionId: String): PeerConnection? { + val factory = peerConnectionFactory ?: return null + + val observer = object : PeerConnection.Observer { + override fun onIceCandidate(candidate: IceCandidate) { + Logger.step("WEBRTC_ICE_CANDIDATE", "ICE candidate for session: $sessionId") + + // Отправка ICE candidate через SocketService согласно ТЗ + val message = JSONObject().apply { + put("type", "ice-candidate") + put("sessionId", sessionId) + put("candidate", candidate.sdp) + put("sdpMid", candidate.sdpMid) + put("sdpMLineIndex", candidate.sdpMLineIndex) + } + onSignalingMessage(message) + } + + override fun onConnectionChange(newState: PeerConnection.PeerConnectionState) { + Logger.step("WEBRTC_CONNECTION_CHANGE", "Session $sessionId state: $newState") + + val currentStates = _connectionState.value.toMutableMap() + currentStates[sessionId] = newState + _connectionState.value = currentStates + } + + override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) { + Logger.step("WEBRTC_ICE_CONNECTION", "Session $sessionId ICE state: $newState") + } + + override fun onIceConnectionReceivingChange(receiving: Boolean) { + Logger.step("WEBRTC_ICE_RECEIVING", "Session $sessionId ICE receiving: $receiving") + } + + override fun onAddStream(stream: MediaStream) { + Logger.step("WEBRTC_STREAM_ADDED", "Remote stream added for session: $sessionId") + } + + override fun onRemoveStream(stream: MediaStream) { + Logger.step("WEBRTC_STREAM_REMOVED", "Remote stream removed for session: $sessionId") + } + + override fun onDataChannel(dataChannel: DataChannel) { + Logger.step("WEBRTC_DATA_CHANNEL", "Data channel opened for session: $sessionId") + } + + override fun onRenegotiationNeeded() { + Logger.step("WEBRTC_RENEGOTIATION", "Renegotiation needed for session: $sessionId") + } + + override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) { + Logger.step("WEBRTC_ICE_GATHERING", "Session $sessionId ICE gathering: $newState") + } + + override fun onIceCandidatesRemoved(candidates: Array) { + Logger.step("WEBRTC_ICE_REMOVED", "ICE candidates removed for session: $sessionId") + } + + override fun onSignalingChange(newState: PeerConnection.SignalingState) { + Logger.step("WEBRTC_SIGNALING", "Session $sessionId signaling: $newState") + } + } + + val peerConnection = factory.createPeerConnection(rtcConfig, observer) + if (peerConnection != null) { + activePeerConnections[sessionId] = peerConnection + } + + return peerConnection + } + + /** + * Добавление локальных медиа-потоков (видео + аудио) + */ + private fun addLocalStreams(peerConnection: PeerConnection, cameraType: String) { + try { + // Создание локального видео-потока + if (localVideoTrack == null) { + localVideoTrack = createVideoTrack(cameraType) + } + + // Создание локального аудио-потока + if (localAudioTrack == null) { + localAudioTrack = createAudioTrack() + } + + // Добавление потоков в PeerConnection + localVideoTrack?.let { peerConnection.addTrack(it, listOf("stream")) } + localAudioTrack?.let { peerConnection.addTrack(it, listOf("stream")) } + + Logger.step("WEBRTC_STREAMS_ADDED", "Local media streams added to peer connection") + + } catch (e: Exception) { + Logger.error("WEBRTC_STREAMS_ERROR", "Failed to add local streams", e) + } + } + + /** + * Создание видео-трека с указанной камерой + */ + private fun createVideoTrack(cameraType: String): VideoTrack? { + Logger.step("WEBRTC_VIDEO_TRACK", "Creating video track for camera: $cameraType") + + try { + val factory = peerConnectionFactory ?: return null + + // Создание видео источника + val videoSource = factory.createVideoSource(false) + + // Создание захватчика камеры + videoCapturer = createCameraCapturer(cameraType) + + if (videoCapturer == null) { + Logger.error("WEBRTC_VIDEO_ERROR", "Failed to create camera capturer", null) + return null + } + + // Инициализация захвата видео + val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", null) + videoCapturer?.initialize(surfaceTextureHelper, context, videoSource.capturerObserver) + + // Запуск захвата видео с разрешением 1280x720 и 30 FPS + videoCapturer?.startCapture(1280, 720, 30) + + // Создание видео-трека + val videoTrack = factory.createVideoTrack("video_track", videoSource) + + Logger.step("WEBRTC_VIDEO_TRACK_CREATED", "Video track created successfully for camera: $cameraType") + return videoTrack + + } catch (e: Exception) { + Logger.error("WEBRTC_VIDEO_TRACK_ERROR", "Failed to create video track", e) + return null + } + } + + /** + * Создание захватчика камеры для указанного типа + */ + private fun createCameraCapturer(cameraType: String): CameraVideoCapturer? { + try { + val camera1Enumerator = Camera1Enumerator(false) + val camera2Enumerator = Camera2Enumerator(context) + + val enumerator = if (Camera2Enumerator.isSupported(context)) camera2Enumerator else camera1Enumerator + + // Поиск камеры по типу + val deviceNames = enumerator.deviceNames + for (deviceName in deviceNames) { + val isFrontFacing = enumerator.isFrontFacing(deviceName) + val isBackFacing = enumerator.isBackFacing(deviceName) + + val matches = when (cameraType) { + "front" -> isFrontFacing + "back", "ultra_wide", "telephoto" -> isBackFacing + else -> isBackFacing + } + + if (matches) { + Logger.d("Using camera device: $deviceName for type: $cameraType") + return enumerator.createCapturer(deviceName, null) + } + } + + // Fallback к первой доступной камере + if (deviceNames.isNotEmpty()) { + Logger.d("Using fallback camera: ${deviceNames[0]}") + return enumerator.createCapturer(deviceNames[0], null) + } + + Logger.error("CAMERA_CAPTURER_ERROR", "No camera devices found", null) + return null + + } catch (e: Exception) { + Logger.error("CAMERA_CAPTURER_ERROR", "Failed to create camera capturer", e) + return null + } + } + + /** + * Создание аудио-трека + */ + private fun createAudioTrack(): AudioTrack? { + try { + val factory = peerConnectionFactory ?: return null + + // Создание аудио источника + val audioConstraints = MediaConstraints().apply { + mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true")) + mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true")) + mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true")) + mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true")) + } + + val audioSource = factory.createAudioSource(audioConstraints) + val audioTrack = factory.createAudioTrack("audio_track", audioSource) + + Logger.step("WEBRTC_AUDIO_TRACK_CREATED", "Audio track created successfully") + return audioTrack + + } catch (e: Exception) { + Logger.error("WEBRTC_AUDIO_TRACK_ERROR", "Failed to create audio track", e) + return null + } + } + + /** + * Создание offer для сессии + */ + private fun createOffer(sessionId: String, peerConnection: PeerConnection) { + try { + val constraints = MediaConstraints().apply { + // Исправляем настройки для корректной работы WebRTC + mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false")) + mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")) + // Добавляем дополнительные constraints для стабильности + mandatory.add(MediaConstraints.KeyValuePair("VoiceActivityDetection", "true")) + } + + peerConnection.createOffer(object : SdpObserver { + override fun onCreateSuccess(sessionDescription: SessionDescription) { + Logger.step("WEBRTC_OFFER_CREATED", "Offer created for session: $sessionId") + + // Изменяем SDP для исправления проблемы с m-section + val modifiedSdp = modifySdpForCompatibility(sessionDescription.description) + val modifiedSessionDescription = SessionDescription(sessionDescription.type, modifiedSdp) + + peerConnection.setLocalDescription(object : SdpObserver { + override fun onSetSuccess() { + Logger.step("WEBRTC_LOCAL_DESC_SET", "Local description set successfully for session: $sessionId") + + // Отправка offer через SocketService согласно ТЗ + val message = JSONObject().apply { + put("type", "offer") + put("sessionId", sessionId) + put("sdp", modifiedSdp) + } + onSignalingMessage(message) + } + + override fun onSetFailure(error: String) { + Logger.error("WEBRTC_SET_LOCAL_ERROR", "Failed to set local description: $error", null) + // Не крашим приложение, а пытаемся создать новое соединение + handleWebRTCError(sessionId, "Local description error: $error") + } + + override fun onCreateSuccess(p0: SessionDescription?) {} + override fun onCreateFailure(p0: String?) {} + }, modifiedSessionDescription) + } + + override fun onCreateFailure(error: String) { + Logger.error("WEBRTC_OFFER_ERROR", "Failed to create offer: $error", null) + handleWebRTCError(sessionId, "Offer creation error: $error") + } + + override fun onSetSuccess() {} + override fun onSetFailure(error: String?) {} + }, constraints) + + } catch (e: Exception) { + Logger.error("WEBRTC_OFFER_EXCEPTION", "Exception creating offer", e) + handleWebRTCError(sessionId, "Offer exception: ${e.message}") + } + } + + /** + * Модификация SDP для совместимости + */ + private fun modifySdpForCompatibility(originalSdp: String): String { + try { + Logger.d("Original SDP length: ${originalSdp.length}") + + // Для send-only режима создаем минимальный корректный SDP + val lines = originalSdp.split("\r\n").toMutableList() + val modifiedLines = mutableListOf() + + var inVideoSection = false + var inAudioSection = false + var currentSection = "" + + for (line in lines) { + when { + line.startsWith("v=") -> { + modifiedLines.add(line) + } + line.startsWith("o=") -> { + modifiedLines.add(line) + } + line.startsWith("s=") -> { + modifiedLines.add(line) + } + line.startsWith("t=") -> { + modifiedLines.add(line) + } + line.startsWith("a=group:BUNDLE") -> { + modifiedLines.add("a=group:BUNDLE 0 1") + } + line.startsWith("a=msid-semantic") -> { + modifiedLines.add(line) + } + line.startsWith("m=video") -> { + inVideoSection = true + inAudioSection = false + currentSection = "video" + modifiedLines.add(line) + } + line.startsWith("m=audio") -> { + inVideoSection = false + inAudioSection = true + currentSection = "audio" + modifiedLines.add(line) + } + line.startsWith("c=") -> { + modifiedLines.add(line) + } + line.startsWith("a=mid:") -> { + when (currentSection) { + "video" -> modifiedLines.add("a=mid:0") + "audio" -> modifiedLines.add("a=mid:1") + else -> modifiedLines.add(line) + } + } + line.startsWith("a=sendonly") || line.startsWith("a=sendrecv") || line.startsWith("a=recvonly") -> { + modifiedLines.add("a=sendonly") + } + line.startsWith("a=rtcp-mux") && !line.contains("only") -> { + modifiedLines.add(line) + } + line.startsWith("a=rtpmap:") || + line.startsWith("a=fmtp:") || + line.startsWith("a=ssrc:") || + line.startsWith("a=msid:") || + line.startsWith("a=cname:") || + line.startsWith("a=ice-ufrag:") || + line.startsWith("a=ice-pwd:") || + line.startsWith("a=fingerprint:") || + line.startsWith("a=setup:") || + line.startsWith("a=candidate:") -> { + modifiedLines.add(line) + } + // Пропускаем проблемные RTCP feedback атрибуты для send-only + line.startsWith("a=rtcp-fb:") || + line.startsWith("a=rtcp-mux-only") || + line.startsWith("a=rtcp-rsize") -> { + // Пропускаем эти строки + } + line.trim().isEmpty() -> { + // Пропускаем пустые строки + } + else -> { + // Добавляем остальные атрибуты + modifiedLines.add(line) + } + } + } + + val modifiedSdp = modifiedLines.joinToString("\r\n") + + Logger.d("Modified SDP length: ${modifiedSdp.length}") + Logger.d("SDP modifications applied successfully") + + return modifiedSdp + + } catch (e: Exception) { + Logger.error("SDP_MODIFY_ERROR", "Failed to modify SDP", e) + return originalSdp + } + } + + /** + * Обработка WebRTC ошибок без краша приложения + */ + private fun handleWebRTCError(sessionId: String, error: String) { + Logger.error("WEBRTC_ERROR_HANDLED", "WebRTC error for session $sessionId: $error", null) + + // Уведомляем о проблеме через сигналинг + val errorMessage = JSONObject().apply { + put("type", "error") + put("sessionId", sessionId) + put("error", error) + } + + try { + onSignalingMessage(errorMessage) + } catch (e: Exception) { + Logger.error("WEBRTC_ERROR_SIGNALING", "Failed to send error message", e) + } + } + + /** + * Обработка answer от оператора + */ + fun handleAnswer(sessionId: String, answerSdp: String) { + Logger.step("WEBRTC_ANSWER", "Processing WebRTC answer for session: $sessionId") + + val peerConnection = activePeerConnections[sessionId] + if (peerConnection == null) { + Logger.error("WEBRTC_ANSWER_ERROR", "No peer connection found for session: $sessionId", null) + return + } + + try { + val answer = SessionDescription(SessionDescription.Type.ANSWER, answerSdp) + peerConnection.setRemoteDescription(object : SdpObserver { + override fun onSetSuccess() { + Logger.step("WEBRTC_ANSWER_SET", "Answer set successfully for session: $sessionId") + } + + override fun onSetFailure(error: String) { + Logger.error("WEBRTC_ANSWER_SET_ERROR", "Failed to set answer: $error", null) + } + + override fun onCreateSuccess(p0: SessionDescription?) {} + override fun onCreateFailure(p0: String?) {} + }, answer) + + } catch (e: Exception) { + Logger.error("WEBRTC_ANSWER_EXCEPTION", "Exception handling answer", e) + } + } + + /** + * Обработка offer от оператора (не используется в текущей архитектуре) + */ + @Suppress("UNUSED_PARAMETER") + fun handleOffer(sessionId: String, offerSdp: String) { + Logger.step("WEBRTC_OFFER", "Processing WebRTC offer for session: $sessionId") + // Пока не реализовано - Android устройство только отправляет offer + } + + /** + * Обработка ICE кандидата от оператора + */ + fun handleIceCandidate(sessionId: String, candidateSdp: String, sdpMid: String, sdpMLineIndex: Int) { + Logger.step("WEBRTC_ICE", "Processing ICE candidate for session: $sessionId") + + val peerConnection = activePeerConnections[sessionId] + if (peerConnection == null) { + Logger.error("WEBRTC_ICE_ERROR", "No peer connection found for session: $sessionId", null) + return + } + + try { + val iceCandidate = IceCandidate(sdpMid, sdpMLineIndex, candidateSdp) + peerConnection.addIceCandidate(iceCandidate) + Logger.step("WEBRTC_ICE_ADDED", "ICE candidate added for session: $sessionId") + + } catch (e: Exception) { + Logger.error("WEBRTC_ICE_EXCEPTION", "Exception handling ICE candidate", e) + } + } + + /** + * Переключение камеры + */ + fun switchCamera(cameraType: String) { + Logger.step("WEBRTC_SWITCH_CAMERA", "Switching camera to: $cameraType") + + try { + // Остановка текущего захвата + videoCapturer?.stopCapture() + + // Создание нового видео-трека + localVideoTrack?.dispose() + localVideoTrack = createVideoTrack(cameraType) + + // Обновление треков во всех активных соединениях + activePeerConnections.forEach { (_, peerConnection) -> + localVideoTrack?.let { videoTrack -> + // Удаление старого трека и добавление нового + val senders = peerConnection.senders + senders.forEach { sender -> + if (sender.track()?.kind() == "video") { + sender.setTrack(videoTrack, false) + } + } + } + } + + Logger.step("WEBRTC_CAMERA_SWITCHED", "Camera switched to: $cameraType") + + } catch (e: Exception) { + Logger.error("WEBRTC_SWITCH_ERROR", "Failed to switch camera", e) + } + } + + /** + * Завершение сессии + */ + fun endSession(sessionId: String) { + Logger.step("WEBRTC_END_SESSION", "Ending WebRTC session: $sessionId") + + activePeerConnections[sessionId]?.let { peerConnection -> + peerConnection.close() + activePeerConnections.remove(sessionId) + } + + val currentStates = _connectionState.value.toMutableMap() + currentStates.remove(sessionId) + _connectionState.value = currentStates + } + + /** + * Остановка всех стримов + */ + fun stopAllStreaming() { + Logger.step("WEBRTC_STOP_ALL", "Stopping all WebRTC streaming") + + activePeerConnections.forEach { (_, peerConnection) -> + peerConnection.close() + } + activePeerConnections.clear() + + videoCapturer?.stopCapture() + videoCapturer?.dispose() + videoCapturer = null + + localVideoTrack?.dispose() + localVideoTrack = null + + localAudioTrack?.dispose() + localAudioTrack = null + + _connectionState.value = emptyMap() + } + + /** + * Освобождение ресурсов + */ + fun dispose() { + Logger.step("WEBRTC_DISPOSE", "Disposing WebRTC Manager") + stopAllStreaming() + peerConnectionFactory?.dispose() + peerConnectionFactory = null + } +} + +/** + * SdpObserver по умолчанию для упрощения кода + */ +open class SimpleSdpObserver : SdpObserver { + override fun onCreateSuccess(sessionDescription: SessionDescription) {} + override fun onSetSuccess() {} + override fun onCreateFailure(error: String) {} + override fun onSetFailure(error: String) {} +} diff --git a/app/src/main/res/drawable/circle_button_background.xml b/app/src/main/res/drawable/circle_button_background.xml new file mode 100644 index 0000000..de89ca7 --- /dev/null +++ b/app/src/main/res/drawable/circle_button_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_legacy_camera.xml b/app/src/main/res/layout/activity_legacy_camera.xml new file mode 100644 index 0000000..55033a0 --- /dev/null +++ b/app/src/main/res/layout/activity_legacy_camera.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + +