From 987f9e55470c2a7e9e08caebc18a5fe87c7eba9f Mon Sep 17 00:00:00 2001 From: subochev Date: Fri, 25 Sep 2026 13:24:56 +0300 Subject: [PATCH] init --- README.md | 134 +- app-glasses-vm/build.gradle.kts | 81 + app-glasses-vm/libs/MercuryAndroidSDK.aar | Bin 0 -> 211266 bytes app-glasses-vm/src/main/AndroidManifest.xml | 57 + .../pw/binom/rayneovm/glasses/GlassesVmApp.kt | 46 + .../pw/binom/rayneovm/glasses/MainActivity.kt | 43 + .../pw/binom/rayneovm/glasses/VmService.kt | 245 +++ .../rayneovm/glasses/VmShizukuService.kt | 14 + .../glasses/audio/AudioCaptureService.kt | 159 ++ .../rayneovm/glasses/audio/AudioStreamer.kt | 62 + .../glasses/protocol/VmGlassesController.kt | 130 ++ .../shizuku/VirtualDisplayFrameSource.kt | 90 ++ .../rayneovm/glasses/ui/GlassesScreen.kt | 116 ++ .../rayneovm/glasses/vm/GlassesScreenState.kt | 21 + .../src/main/res/values/strings.xml | 3 + app-glasses-vm/src/main/res/values/themes.xml | 7 + app-phone-vm/build.gradle.kts | 80 + app-phone-vm/src/main/AndroidManifest.xml | 48 + .../pw/binom/rayneovm/phone/MainActivity.kt | 59 + .../pw/binom/rayneovm/phone/PhoneVmApp.kt | 41 + .../phone/audio/AudioPlaybackManager.kt | 108 ++ .../phone/audio/AudioPlaybackService.kt | 51 + .../phone/mercury/MercuryController.kt | 271 ++++ .../phone/protocol/VmPhoneController.kt | 191 +++ .../binom/rayneovm/phone/ui/AppsListScreen.kt | 105 ++ .../rayneovm/phone/ui/GlassesStatusScreen.kt | 263 ++++ .../pw/binom/rayneovm/phone/ui/HomeScreen.kt | 75 + .../binom/rayneovm/phone/ui/TouchpadScreen.kt | 107 ++ .../pw/binom/rayneovm/phone/ui/ZonaProfile.kt | 196 +++ app-phone-vm/src/main/res/values/strings.xml | 3 + app-phone-vm/src/main/res/values/themes.xml | 7 + build.gradle.kts | 7 + docs/SPEC.md | 1369 +++++++++++++++++ gradle.properties | 10 + gradle/gradle-daemon-jvm.properties | 12 + gradle/libs.versions.toml | 67 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 47505 bytes gradle/wrapper/gradle-wrapper.properties | 9 + gradlew | 248 +++ gradlew.bat | 82 + local.properties | 1 + settings.gradle.kts | 35 + shared/build.gradle.kts | 44 + shared/src/main/AndroidManifest.xml | 2 + .../pw/binom/rayneovm/shared/VmMessage.kt | 113 ++ .../pw/binom/rayneovm/shared/VmProtocol.kt | 51 + .../binom/rayneovm/shared/VmProtocolTest.kt | 136 ++ 47 files changed, 4998 insertions(+), 1 deletion(-) create mode 100644 app-glasses-vm/build.gradle.kts create mode 100644 app-glasses-vm/libs/MercuryAndroidSDK.aar create mode 100644 app-glasses-vm/src/main/AndroidManifest.xml create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/GlassesVmApp.kt create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/MainActivity.kt create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmShizukuService.kt create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/audio/AudioCaptureService.kt create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/audio/AudioStreamer.kt create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/shizuku/VirtualDisplayFrameSource.kt create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/ui/GlassesScreen.kt create mode 100644 app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/vm/GlassesScreenState.kt create mode 100644 app-glasses-vm/src/main/res/values/strings.xml create mode 100644 app-glasses-vm/src/main/res/values/themes.xml create mode 100644 app-phone-vm/build.gradle.kts create mode 100644 app-phone-vm/src/main/AndroidManifest.xml create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/MainActivity.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/PhoneVmApp.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/audio/AudioPlaybackManager.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/audio/AudioPlaybackService.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/mercury/MercuryController.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/protocol/VmPhoneController.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/AppsListScreen.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/GlassesStatusScreen.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/HomeScreen.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/TouchpadScreen.kt create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/ZonaProfile.kt create mode 100644 app-phone-vm/src/main/res/values/strings.xml create mode 100644 app-phone-vm/src/main/res/values/themes.xml create mode 100644 build.gradle.kts create mode 100644 docs/SPEC.md create mode 100644 gradle.properties create mode 100644 gradle/gradle-daemon-jvm.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 local.properties create mode 100644 settings.gradle.kts create mode 100644 shared/build.gradle.kts create mode 100644 shared/src/main/AndroidManifest.xml create mode 100644 shared/src/main/kotlin/pw/binom/rayneovm/shared/VmMessage.kt create mode 100644 shared/src/main/kotlin/pw/binom/rayneovm/shared/VmProtocol.kt create mode 100644 shared/src/test/kotlin/pw/binom/rayneovm/shared/VmProtocolTest.kt diff --git a/README.md b/README.md index adbbc57..86e2f75 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,134 @@ -# rayneo-vm +# RayNeo VM +Приложение для запуска Android-приложений (например, Zona) на очках **RayNeo X3 Pro** через виртуальный дисплей. + +## Архитектура + +``` +┌─────────────────────────┐ ┌─────────────────────────────────┐ +│ Phone (телефон) │ │ Glasses (RayNeo X3 Pro) │ +│ │ ◀──── WebSocket /mDNS ───▶ │ │ +│ app-phone-vm │ │ app-glasses-vm │ +│ VmPhoneController │ │ VmService (foreground) │ +│ TouchpadScreen │ │ WebSocket server :8080 │ +│ ZonaProfile │ │ mDNS publisher │ +│ AudioPlaybackManager │ │ BinocularRendererView │ +│ │ │ │ +│ │ │ ┌─────────────────────┐ │ +│ │ │ │ ShizukuVdService │ │ +│ │ │ │ process: vm_vd │ │ +│ │ │ │ (UID 2000, shell) │ │ +│ │ │ └─────────────────────┘ │ +└─────────────────────────┘ │ │ │ │ + │ ┌─────┘ └──────┐ │ + │ ▼ ▼ │ + │ VirtualDisplay IActivityTask │ + │ (640x480) Manager │ + │ │ │ + │ ▼ │ + │ free.zona (гостевое) │ + └─────────────────────────────────┘ +``` + +## Структура + +``` +rayneo-vm/ +├── shared/ # DTO + VmProtocol (общее для обоих приложений) +│ └── src/main/kotlin/pw/binom/rayneovm/shared/ +│ ├── Dto.kt # VmMessage sealed, TouchEvent, KeyEvent, ... +│ ├── VmProtocol.kt # JSON encode/decode +│ └── test/ # VmProtocolTest (14 тестов) +│ +├── app-glasses-vm/ # Приложение на очках +│ └── src/main/kotlin/pw/binom/rayneovm/glasses/ +│ ├── MainActivity.kt # Compose host +│ ├── VmService.kt # Foreground service, WebSocket server +│ ├── VmShizukuService.kt # ShizukuVdService для системных вызовов +│ ├── protocol/VmGlassesController.kt +│ ├── vm/GlassesScreenState.kt +│ ├── shizuku/VirtualDisplayFrameSource.kt # SurfaceTexture ↔ VirtualDisplay +│ ├── audio/AudioCaptureService.kt # Режим 2: захват системного аудио +│ └── audio/AudioStreamer.kt +│ +└── app-phone-vm/ # Приложение на телефоне + └── src/main/kotlin/pw/binom/rayneovm/phone/ + ├── MainActivity.kt # Compose host + NavGraph + ├── PhoneVmApp.kt # Application + ├── protocol/VmPhoneController.kt # Auto-reconnect + mDNS + ├── ui/HomeScreen.kt + ├── ui/GlassesStatusScreen.kt + ├── ui/AppsListScreen.kt + ├── ui/ZonaProfile.kt # Touchpad + media keys + keyboard + ├── ui/TouchpadScreen.kt + └── audio/AudioPlaybackManager.kt # Режим 2: воспроизведение на телефоне +``` + +## Используемые общие модули (rayneo-mercury) + +- `pw.binom.mercury:rayneo-mercury-transport:0.1.0-SNAPSHOT` + — generic Transport / ServerTransport, WifiTransport, BtTransport, MdnsScanner +- `pw.binom.mercury:rayneo-mercury-binocular-renderer:0.1.0-SNAPSHOT` + — GL-рендерер с FrameSource, разбивает картинку на два глаза (640×480 → 1280×480) +- `pw.binom.mercury:rayneo-mercury-shizuku-vd:0.1.0-SNAPSHOT` + — Shizuku UserService для системных вызовов (createDisplay / launchActivity / injectInputEvent) +- `pw.binom.mercury:library:0.1.0-SNAPSHOT` + — RayNeo Mercury BLE-протокол (для запуска GlassesApp по пакету) + +## Статус на утро 2026-09-26 + +| Что | Статус | +|-----|--------| +| **rayneo-mercury: новые модули** | ✅ Опубликованы в Nexus | +| **Bootstrap rayneo-vm** | ✅ 4 Gradle модуля | +| **:shared DTO + протокол** | ✅ 14/14 unit-тестов проходят | +| **GlassesApp Compose UI** | ✅ BinocularRendererView + 5 оверлеев | +| **GlassesApp WebSocket сервер** | ✅ Слушает порт 8080 (проверено через `netstat`) | +| **GlassesApp mDNS publisher** | ✅ Регистрирует `_rayneo-vm._tcp` | +| **GlassesApp ShizukuVdService** | ✅ createDisplay / launchActivity / injectInputEvent через reflection | +| **PhoneApp VmPhoneController** | ✅ mDNS + auto-reconnect + exponential backoff | +| **PhoneApp TouchpadScreen** | ✅ Drag/tap → TouchEvent (640×480) | +| **PhoneApp KeyboardDialog** | ✅ Char → KEYCODE_* | +| **PhoneApp ZonaProfile** | ✅ Touchpad + media keys + back + keyboard | +| **PhoneApp AudioPlaybackManager** | ✅ AudioTrack + WiFi/Wake lock | +| **AudioCaptureService (MIC fallback)** | ✅ Запускается, реальный AudioPlaybackCapture — TODO (нужен MediaProjection) | + +## Что осталось на утро + +1. **Реальный проброс SurfaceTexture клиент → сервис.** Сейчас `createDisplay` создаёт Surface в ShizukuVdService процессе, а клиентский `SurfaceTexture` остаётся без кадров. Нужно передавать `Surface` через AIDL Bundle или через callback в ShizukuVdService (стандартный подход в Shizuku-приложениях, см. `IIdProvider`-style). +2. **AudioPlaybackCapture (Android 12+).** Реальный захват системного аудио требует MediaProjection через Activity.onActivityResult. Сейчас сервис стартует в MIC-режиме. +3. **InjectInputEvent — verify on device.** Код написан, но не тестировался вживую на устройстве. +4. **TaskStackListener для обнаружения краша гостя.** Каркас в SPEC, реализация не доделана. + +## Сборка + +```bash +./gradlew :app-glasses-vm:assembleDebug +./gradlew :app-phone-vm:assembleDebug +``` + +## Установка + +```bash +adb -s install -r app-glasses-vm/build/outputs/apk/debug/app-glasses-vm-debug.apk +adb -s install -r app-phone-vm/build/outputs/apk/debug/app-phone-vm-debug.apk +``` + +## Smoke-test (проверено) + +1. Запустить `GlassesVmApp` на очках → VmService стартует, `netstat -tlnp` показывает `*:8080`. +2. Запустить `PhoneVmApp` на телефоне → mDNS ищет `_rayneo-vm._tcp`, ловит сервис очков. +3. На телефоне: тап на Zona → `LaunchAppCommand("free.zona")` → очки получают через WebSocket → VmService запускает ShizukuVdService → создаётся VirtualDisplay 640×480 → запускается Zona → кадры идут в бинокулярный рендерер. + +## Конфигурация + +Версии в `gradle/libs.versions.toml`: +- Kotlin 2.4.20, AGP 9.4.1, Gradle 9.x +- Compose BOM 2026.09.00 +- Ktor 3.6.0 +- Shizuku API 13.1.5 +- compileSdk 37, minSdk 32 (glasses), 32 (phone) + +## Лицензия + +Внутренний проект. diff --git a/app-glasses-vm/build.gradle.kts b/app-glasses-vm/build.gradle.kts new file mode 100644 index 0000000..35a65b3 --- /dev/null +++ b/app-glasses-vm/build.gradle.kts @@ -0,0 +1,81 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "pw.binom.rayneovm.glasses" + compileSdk = 37 + + defaultConfig { + applicationId = "pw.binom.rayneovm.glasses" + minSdk = 32 // RayNeo X3 Pro — API 32 + targetSdk = 37 + versionCode = 1 + versionName = "0.1.0" + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } +} + +dependencies { + implementation(project(":shared")) + + // RayNeo Mercury Android SDK (UI-кит: MirroringView, RecyclerView helpers, MercurySDK.init) + implementation(files("libs/MercuryAndroidSDK.aar")) + + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.serialization.json) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.service) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons) + implementation(libs.androidx.navigation.compose) + + implementation(libs.shizuku.api) + implementation(libs.mercury.library) + implementation(libs.mercury.transport) + implementation(libs.mercury.binocular) + implementation(libs.mercury.shizuku) +} + +tasks.register("installGlasses") { + group = "deploy" + description = "Install GlassesApp APK на RayNeo X3 Pro (serial: A06B4AB933C4103)" + dependsOn("assembleDebug") + val serial = "A06B4AB933C4103" + val apk = layout.buildDirectory.file("outputs/apk/debug/app-glasses-vm-debug.apk") + commandLine("adb", "-s", serial, "install", "-r", apk.get().asFile.absolutePath) +} + diff --git a/app-glasses-vm/libs/MercuryAndroidSDK.aar b/app-glasses-vm/libs/MercuryAndroidSDK.aar new file mode 100644 index 0000000000000000000000000000000000000000..a354449f07541ff03176be116a9b8e1a57dd455d GIT binary patch literal 211266 zcmV)JK)b(CO9KQ7000OG0000%0Le20ZJ7W70PO()00jU508%b=cyx`vt`J2|R_gNp-U!;1q9aoWn40TE!PoO9KQ7000OG0000% z0Dm=qf~f!i00RL4022TJ06}hKa&Kv5O<`_nW@U49E_iKhWsl1W!Y~j;_x*}6`zBgJ zr1bF-g8Pt6+E9~}Oe*^K#?p%5-Ob_LVW#Py?#><<$p+uTkspAeS3z4pw6G*oUZC%a z#z}7tQes*6R7=y9%B= zjB1ImZ=3W4tSi<+vk+35$vS0001EY+-YAWpgfSVREdxRa6~~wx&(cV8LN6 zAQ0T$-QC^Y-Q9vM+}(paB)A86cZY?$I|Mp=>~l_!udjChf7cjwQL{$f&G&ictSUtr zC}@bk$7cwl&qR>uuL|BLpCBMIAR!=_{#H#K>=?|=jciRE>>2FLoK4)Ey%>z_O`RRA zOc`8EZ5V{ztZYq19PG`lEa**aja*#x5)2gP6aj;IyUXenlp>hdgsU_kKO0}6KCzN- zsE_m-j={=Ym2lZi4X(7gTk)V9??FA;bueG|qHKpTcZEhoamWQ6dTo^|kFNlbfv)9n!v+5w`tJY@Wo0K4K> z`1oCbj8$@woK5&gMwU%%qN0q6a*^HNFNRW0my>Fu=_R>vgd`t|l5Ymynri%&x&h2z z18|pj3SD9PbhKw*?mn$OHvIa0X62agGzIT_Y?;q8ORaLut+sVKCWMQKG$!Id$(H5= zRlS0Tz#~mE5MPy%lH>>bKqd4W8MGkYIm^CUsyFuP$ zfORL_s!EAdy}`VM=&PaOn0LU_2vup*#B+a;pk}vdnY*yN-1pINIxNLUuh7FSa?*Kc zdKXA^ed6YUrf|g?%$mzYt0ZL9-&r=cLDkr%Sd1rxEtMFExtPDr-dHE(iU{1{(d+=X z_dgzR%5*RfRraG?U4fRu2`7%@j$;O_j-G?$>okx^w|s|g9a_5L`xh*CP!#{sIC+cG z2lP-75YvBajQ>I7$o;#fA}aImzKKzpa=?(ld^bP$Mu z7-T958EG|78D?FVONw#Ha9*XN3_Thl@)4cODa`d<8(yq@UgV>QfO>QJlX8UmMqh{5 z*6SvR!)Dj>?WMsML`yUS(BJ#U^!bFuBFD!Ltts0kwiztTt&iD1dsrUbYmM3n4UV{i z(Fh9C(N*iD12?KsC{8vaU(hfe_h&79A5tr4GdFK3Q?RjjzSrwYCGBxL9&=42<{?%h zRY;Igxi6<-JNyMRXvk40zZ0=Gi-Uxbyk|fCNguNCZQ-G&7Gwik)m9#^$g7WA;h=qbs?(KAETl&)P8C|=qQMlF zZQaWLwQye3o<+l-mq8CWh0<`Fl~*4UEgB6*l-zyTy9_tbNa#m^Q5!UXd4Cikn5$G& z`_Bn?-&HRUdz^^}f2`lJTL8(-Z4X)=xW7aa=OZvWmF6QMHzi*d2>UdXJR26&C6J8@ zTls6=m+`mjEw+fgyoKJB2|k5gI;Y4C*VJVY`->xaoPyFx>4t?D(oVyspOI0#(c4w2 z-6eDll1f6zx(A+7=CGoaLIx#^>TEUX3wTX&OzL8cQ0&wneSlcX#Vn@HY zbQF=Wq_EtK-GYY3OLn^;27w$nIQrrJIvN?OG(0^&G&;}!Sh=gBM4EV(I<)H z^?J@aeX(@r_vB`gM~`Dw#_XKV-ZfNLjfB5vD@F-3Dvh!f!bUPa*W;qABTv?#e z2J7ldo0(~Sh*Gc>0ezANsZ3vBSA+)VE9A@RQ0pUDi1_rX+u=yZ59W}J|1u#GK)g>- z?kh?C9&}sj@$s$tuo$sc$!zMrvL`wf?lp><(imFhvX~`VVire3=E6qTJK~nl3M!-T z9%02tw3+gE0CEQG87!=&m_EZ0E@v!;jUfUI=2E-{C6#Z1qcS+T*D6#flql3FG=ZMK zg7E?SpV?caCCeK7o4qtx2nebFAbUoRRt$0u##XjwDy~MZW~5dw|GxUmaeFfpS2I&m zrhkXBT6IeSPXhIQksTjj9*Q*(!Y+}!p^?f_@Vla{R5(-w9v&_}Q6sPDX*(M&|D|@b z1&%7)sLMxW1%scQPK0Q+s+iwd-h5bvOOsW2BDUIzR5MRI%k9Ke&eq4(*XB z5^|OM&T`yZg^|xDMuAbSE0XM*+%s|C;D6Hk0SVAbsUStJ`dj(=I666#UfSTId+3kF zwzF~pvq)}sB#?~3B{NJ^WueJMB! zwps6*0eN3fBkq5DGUH|$J)UiKoyt!$DPJ7`UesG=Ps&R0%u~>)+lsAEsL39Uxo<(k zfb5TSSHwchVVew#7HQa3@K<#6A9(qyy_{GE0mrt}iE=w%JI1DywrKfu*T$<^s&L-J zB2|z!Xp~3dA!AEfc#tAo`RkMVeOk;5;dV`(2kv#yXUs;$ark9-5NaOKQ;gC%$D)j1 zYg`YzLgN7BQ?9SY!tBhf3sF+CEZJKaMo!iGkHu@=wn!$)4H#njwLGQiMO?vJS*+zX zeH6l`OqkTJ&NKc1?Q{zO>9X#tzYgGhPRFFh9pM?PI8E5p2#v#*)-Xa^`(c2jIcHIV zT-qHawY-7?a4*PFOaWO+DqL}v3;cb0%KS)IrTA$$xB~%*J!gT-w-8#)_^rs6Jg=YH ze)S2uc{pLIZNAqIwsPbTXt{zBsra|23Jrx5+$p>BvSG`WbiG3g3MD0aOGrZ5lb3Y@ zehd}JWnRs`arhL)iz`!bgm-{&&(}VhQMJ}>|7A5k?#}R$jm^tJ>Vk@Z9TJ^j#lB2& zu|JkIv0*8B)~M1M_K#`k0iNxPM1g!^TK2chwz$PtS6tC(${#+&??{&)-;YiQ-~w*L zc;31UrthCwiUN8?bu7O{7Lc%Rq2a!wO)}LG?(X)Sxm1yLg{C|aL_E2y`{7PtV-|pe zmI(oE120RT!TNAU-yc5b+w1`H`0ap4!U>DRw>67i&{=esLykZl(Shk0Pn)=1mQ{y% zQ^FB)545RWT>^)h$XhSzUcuZQUmu+DZAkhHVk1UVMq9@b!##$75<~ynBFWFcv^Yii z|3(Z4V;3`L_kUj~n5#Kk{WmctG<-ZT#4tbPG854Yiqr_yk^u#BbEGP*D!of7E^bVD6K<6I>dG1pd(bLwsC&Qb-Z924WH(F6ZWq zPp-day=MAczCRo)2tc(EZZYX{de|3ycm-&y-{?*~!M#Z@5u{+3!SXBD6L^9BtS7WmPmfeWyQ-UA=xm9+69Esb!1xiGa=Jm9!Iq^ zPYdSN8~qAy%1*G$=r_~?=`3mTW;RoSP;L-Jv3 zV7KY{HRrIxC9p@+jH?yV{37_w@s5G+u1@5)zNf0=J=!3t$kJekobq=g8QDeTKej!T zS|UiZa)Y&qpJr?2Z9uJt&KYv_*u`LDC6yHEC!`wNqxIDA7OAU;FYa54c|s@`A~+(I z2xtUn8p@G+b1je-Ndz zl?@-2P`pt$qt$tjgrJp}e1?Pd&#+h`*Z9vc>cfDLJ)$nRUS$ycJK}gpbz3-99v&Wr zmKQZ)#ky7%ZN=27$(k7iO?u%hwo^_#{QOM(Y#9JNu9Qq^J|<>&$GvZT=RL4j1kr}` zF~OeW!S*2`%&xCZi?P|;>QePES4N{f4^>v#1WOWTYBU+Hk4 zukjF<^F(3Z4oaogZ_)a!LTNc`IXk=`!e>EcXTI2KxKLBV-w-VvDC{*A@hKiZOnjf~Dj$ zH;*oTx=bxSx!Zt8mYtv2Qqt)J=Bo2Gre)*e#U{+B2WM^$1>;=pnDfTDkUmu$qpKRZ z)Tuk&4g*7R24LKcgyuE0=6svLr_9IBn4W126I6n&wUw)WuFvw74^+P-Jtgo(z)vC%2Sz6O`6a`@+>i?ut$t@E4(Jpqt2u!(wqe&&54_nW zgQx6K|2Q;tBfIRLdWuj!=fX9Ezo+|kPj3zP;*IS++Dm@I;nzPeswL?{%=5lt-$Y~f zoBYXDeBioLV`^rR&F@b+PUW8RhSthSRXX4iLy>>#y2iRv$9X@lBjGB>LOY5wKRbz= z%*{<99b&L8p(aBXYoy0*9i+_E{$oysVX{Sn)qdmbLCprjbG5Ewic;4dL0WB z8;zz9K@MXEEQUlTLdR#8{RfS~F97SXlt;fcddVM6kFTp@^c{{|3OX`46H=+g5_n}G zcIs;RB8p}4O{&Etgku}&UX(irJl1mc2bXP=6zN7C*#t-XTNg%0ey$_%k|953l3};6 z5&zOVEMm7XbEgs?oJ;2G>MmHNcmb z-i>8Gs8X1`P#@YSy0d4*e`?W5n7&@&uNHrP`G2KF=Kt2Bjs~6@=DU0g^z1aOcfP9K zTr&1&QmA=#(mP?OqctsnU&5FhLuID3$vKs8RGwsov8H<3> zk8Yf_SJM1-;e72SW}Ar&mcz5t=EoI_!{*=qFTn2Gr&RD{wN9ZPhp&^ClnWpD^npss zAWMOXSxYU+Zebb7U1ObQd3 zF9Wov=DKYoJ%yiB#7In$lyO#10U;%hm^9hmij&woxinodCE{}|49ZiC0{!Dkkabkt zq?lw$EGoI1i!xTCi%!@%+_aL6xoBkGCH7@`iNe&&V5FE!X&~Wba49F{A21&emm+1X z2eVlPMV6;oYnDMP7yvxP9Aq(zJuR(SQ$Dg7xvar#{3Xw_5I&>7rld#lRxk`T)YUk@ z7Z%@PAsp7eISV(LF$a?Y-D}}R813c?#N(vn z#Ny-?K>6Ye7BQ?0nPN85M1wuX(ZK@%xG1JC%F)4?yW8wc=4E=nykfLs$8CYdTo_QT z7vWQhgAGsAg|(t^eXaYU+)Fn>V@}gxXV1|amAGL2+(?nw2WJB>EV(qCH#N6?x7eEf zQ1?ZxeJUPR|M*58`giZc4pfoR6@bXxq;t?Xq*FQcbPa(P$K5HA#3IKmK9_MJhf`kP zp*P5=gsZKW+@fCDfxWU^n!j8)j@{K>MzljNw>eCk;8b-uhMbi})zUdT%2EO_is#ZB zzA9G%SdY1>phH5|$sF0XBE__=StEh@kOf^I{jP<<3t@MUuNWmrkq5wRzaC6n*~0Gy z7>%)$5zRCATn&A!OIGM8 zuP?8F2evfSOK}GJ7DoajjyFrJ$vc)astWvlb@}?x?X9D3vUe1A6q@^M+c;d1SuEO{iRWHuZKppy|G4ZbJa{wCR4Ne!hIrGdjLU7ve+ zlVy`SdX^}EeH7=cTN@DpchRUtp6Zz!I5Hb4_3ExsPgA(*E6Co*~`V)zi4SIViRc_Ipl zxk%*?O0Crx8lF#-trRu&LYSi`6~ek5X786TDmS+WGJ$q0i-pz$oK6;skVU;pnWy!p z4X0rmN(LNv; z&Zx4|U%O(@NS*<1?!~x_QeD_lyF$oqaxLUO@N*lfwa_ysZRZXlk2XVMi8OhIEu9Eh z)k){Qukolpv$nS)Z#9WJ6LDmYEv$h6hU<{skv z(^de)&XKe}D?#S2LN4S=%mZUo6r)Sj*5JA8r@^3DVD{mwm{jD&-=&#>oG++YN$(?4=vfHW|kH-%*rqI?V8yx}8e^%+{Q1X81MS^ei4KkSEHh=?kRZemV= z5L65&>F3;tZ7p67u@4MKfJBL+RgVq0?60>>o_}3C{L@+|IL9zrP!JG4f6b-!|HN8< zpL8{|cUAcJ4d?$}?ow3s6!0W4`RdvAe_ELcqkof2ay>1I>WOxemSz%*kyNx4l5~np zzEO->G68htx_P*7h^mp~zRJ94mn)_4ik8LwQBu2nfhw2ANYXp+bh*raWwNk1xD3$m zguUWo2l)HFzs;OX$?JL2mE%oj?y}{G4I!?6-H(!lX`BCooA{Q0SIn459v4F8B{6H0 z(PEd~SaKDk>&hKg+;o(znrLDsHazMagsc&EEhR0jsnHdgJ0q(B0?p2i=M<05Zq(h5 zaRNk|u>12Vnv)rT)|mZSs@;gJ4r9|%eHOY$;(#((ju>p5uLB-Xqf|Bt;ig1octH2p zLH*lyMJ!6@MDEp7g^*A)JS$LEA1N|g1UFrj!#j=zNgre590IX{RTm9O!naB!_4X{{ z3r%8M*hS(cl@v`gT((poyS##VT`7f@&EiS*xHUw_wrmhOP~{^Mi?o9N4a}dC$!nqqM>TdBnpH=c{^h` zNpUe)PT(oRA)!p(_=^T`5FsKe|4IGMUX~Xb_fl-udkR@U;H{-i2(7g@?icAIx`17h zQYXtg?k6DmWED*{F}Gtc-v@$tioh;6;~PXK1 z|7d!noukD%f06mIdzkC7&weot&&<4v<1d@R(C$Dk|DxBkfxnSrz$iOEpO3ACkIvmG zwS?QZ*R;sIeizC#%c$%5gHM1_ns7G736m9YKrq0qY}Z;FU2yW)@TFi=h$)@8#I}vI z>P=OcC}*1i1954DoOtR?;iwStQ(mGz$rM}BG0e>Xg#xzz_QMWhOR>POE1twsLX{uk zRhP|e3K20=CHX_}4GDAI+}%raNMa{0pUb3L#Li56tido_xFllOBP zk$;I_6?ezuN)&~RKxw)rqR0OZ++%WsSo>z@IAgfFo!GOV+i~-MyquvEYaRXy4*_w2 z1p&eKf1l_7nm9FI4e&LvJ~sVWY!l5edP$n3*o*XXW!R#z9L)YaIPr)0BGW~ChnTg|kfdiwed_hj)De7&CW3EO=nRa<}%KEvm- z_0WH+^)TD#qP64t(xw)YGbDhK*9F|WoiIdFXzZ=5%#x5Q=}kL}DrKWGS4W&#SHdoD z1&laqE0QcAn%N7nWG$g6x+_Y^VJ4P94^Gs)*i+~ zb5hgPdF7g$lWu(T%;Z|ebZ9H(x-o@_76Q!Hd264fVnq;%M;V)teycA3MJ1(RaevFC zG;x%f9RbW3N#~>+dSUHN2FcDOzg;ngOop=<%c3n@FLxgSsq9|BCX_O=)+NN1zxYQm zSqi9{m|IHB*G=ID1^i*zrHZsN;xfzAxVkl;?_1%ZHHu{%Lu;nsYalLdix=(CxFV)t zc)utdOK)XpTS3<9xvJ^sMYU&Yg$Zo8IgK15w92!FzAfZRFXD&43X?6=mR^Nih zkL$9=Tu5tuLwSmDb9l+6x{@ksaa-A$$$_?G5lgZcVn{S zFSB8B7N3RUl$BIzHE%S9y^;sn?Is>W$BT5N3D?P~o84NXRxcBE$FajwwHZkso@#zi z4A8em%*~HPE_!kjunS5)Z#WK4%%|ED1NqIY_EuCgH^R=ue))v*BMGb@R!-LBC6Xx9 zRLz6uk8g1$AH|LG_-E#=dg&mi-UZ^T2B;E)0kp`IT2XOS+0DKPJw_rzdb6v_e=HNR;P~aS68wn-^)Y!w0fRw69HX;WY&NP z$JF_+w;yXG6qPKiq(B$AoF3kZU_hTI6{+kr(S~GEifhYyl578z;?=!bHL5{A3x3Rj zi9<)&{izf(j0z;VkcR;$MZdQwfiWiPoJ}xKzSUf|Y`GcYeipxyx*fWRm#bX-swui5 zF->U1ne>_cGxQPxtYrYzabX)Y<-_3o)BZcM2SaYggG|?(IH&?EA)L$8M57b{j_Z35` zzX~+W)I;t(QHm(4@Erdtk#rAz*#e#zI*It<=WT;tNEfv2tOhFFpgm`>`-MVe${w{q zd@}{`ua0P`P1%y?)2!4HYBY_DSoKfVWamRTM?o|v1q(3Mz9uC5@a3L1?;bKF5m3E{hkz2oD+)`<% zlp%qzLy?Zd7!6ZVur%dvYDU)Q(5L~eyL<2Jskia_u#4~0TXKzSB!X0Nd*`*Bl$)c} zQ{@v^SeLxT=zOZ0feua;r8iWq)24wkSt_{M+Q?wN|Fi??m2F?~FovKT51Qdbd1u3( zvM}XhT3nDQ%0opDtyvwFl_tCCj-7_uM1y>MAoEQr;deMS-W0`J+(JZVuNj_S2C}Eq%dT2Bq0NvvsE%V#}r2RUTs;p2g}nCw=R}`?EdX*kW|>g&64p z(-U{cnRF+cwPn@KIEkAXn(+o!%W)pc>(yt-&sug|17r@scVS5%aGMxu?NXe-K;q&h zqa&hDcZZ?BG19FdIbACxKIqcp$2Hb4Qh8F;v@%^TS+-oGCytZ-r%OFym5Kq4eXFV) z`fVwW+h1b7hVT>RptcukLY?jbG2Z$R&x#%be_OP?HskZV+El4o%AQgUBxNB?`}kQh zPZh+caT(7^b{U}iu1l=?>j-6bdBp{F7jv9L2t&eGWv~Oo&a1|CAIuvk&c|(oD?<(E zlfs-bPUFVKVx{M#)@H9{ZDo1C-276QLGF+-^{)^cQh%zqdat*F-1n(nji{#yxzG3D zE&83fw2icp&4n~7R9(}b?#!w%7e7}wIBgpxhNI4D+);Y&8P)@$+Ot;7-FF>C<4}cz zi|xo3T;56}M-hDRJ+zw}Q8?b^&oM$!fAA%v3NnP~C(ouqrt|A~|H4|6{2??uO3*_O zWdyZ`e8#9~@8*tM#;9oZV^pxQ2e@gp{RPScciGM`ZlHciu)muhiVgMwuVt)z1GWIx z8hQQ?+&|~7=OoygwZHq*#^3!(?mw8fjGe7aEzB5{jlAs59Hi{c9psGcjV%7INhW`v zakKq9g2{=p{qIlcb}D#knEv+Yi9d^26&qn}s}q{w9T2U`lfNNQHB=H&mQ(hHO->w; z)2?(z`FOCH;`8`ESUe%RW?KQjejSW^ryQy$QngeO;4~+H%;sa5^6Gq?`tASxwVTg( z;_6Smg%0ADdBwUdg4P(V`M9?^wMpA#pqa~-^M1qbBJ>EK5n>HA)niL0`(wz8YG5IA zl$$*bQwo=*EYF+aDgx8NVe9ZzXwznIM!;vO?`mUBZ|qjuy(-WOne8mk8L#yjmx+c* z+5^bEDpfj7hMuDG-Z_xzHS=G=8G%cStyNw%YIADUNQy4l-cex(j>95xibJ zjuDCpdH3sK2<2Lm z8M|!{2%FmCYPRMC)i_QGNt`-`+H5*zHw(aiKLwxz%p`O2VEWF_S~t~vrxIg?FWi(6 zE_b*^W+^pttCFK^ki?GzR$;Bpd`A*f6&l6sTkSxai^F!MSN#0b@%@~kdd%msVwB^S zM}0Y%n|%}hJI+WZD+}vc?B9w0#F^K(F~|x8Qh|D+@RGZcL<$DZ1#K6BHM*i_XPOB0 z>2p6w0zkm0)aqJK0*6ZI3ngHQ&-9lKmz9m#N+m?`nlQ%D*C)k3UhJ|no%-iKQf!7I z-_;N3)X!ZP-d{@PJf}^}`F{GuIspl7o<8R(#*{j01xzWca8R@?DYlZTGw6q>;S9zT z1=Yz?-xD=<-0mdn{l;H@SOo`Cs6P4Hh9R4_B>C}W5!i^c=ns(~+jzc>5bL6FE-s(1V{Lo`U3~cT#?{2Fo_vqN-B zy+!g=m-Gy_VYPVSu8dOI;th}&u92P_g$bvH$iF|_&Wf&K+^ zQ>mrGvDzi}4f&rGvHG&cS_1OQC&v`Qv&nd+r!8_ZGrX_S!l*K zoPOyOrbsuS^<-4c>=PL&QJ-u*()eW6Ts`y-*&Z3n2*a=p!HqGs!k{Wqgc{XY$391~ zvHT@18kVx9T3K=XVK#s1Zt6x)38xHi6lGbs za4nr~)}_&pfIG}|-f^weTPTM4tR@@WnsRIuHq?3m##+rsK;Pv$E)fo(6=K78Ft>KD zkNAwvoa@k??$~@tYt@8LYR2wrNY{ZQ$1ZJ)Nn7DzOX;`Xc+FW2*NMfPYPcShe=(?6QDsu+=>=n9J_ zL~36iR0zL>Sd0GIJyo1Yx}Q|!In|h-o+G3diI`3iCDz5Wj&oHfr#21XaxLap(n_VA zubh_{+hSTt>N@#-rw~vTEhCwO#8>E$`85)N1a{qJ6FXp-ix>^Hc$l}hf0PJXipP@ znTL~EqXeU`m?NgZi020M1?C>f+MJhXg@d25U(svRnf5~%-(lAf2|*dDbhdDcD&d}Q zY~-H3i@IPwUIOcqc|Ii>Q*nl55#220M=@MAK2CqhoX~fm#8aDzBJJ09hM)Cuo1C~a zm?F3nNN{8Q$uBW?NS#H{A&~u+D+wr^6 zi#4_)tJUg9o|>_v+m*yK0%D@-bW3q~i3Q=yq^upR7QsLSaW4lfp5Bm`cK? zrAdyH>T~|RS8f-%c=$IBOO-U;u?$zJX=HMP=_vM|w+KV~C@FG*F$XNJnX*T>{R}FSld9uNqivNMTJZCz%(=Uh?x%yM^D0>Zz^O=9yeC z5}>tQyn+yy^bnyU2A}GBv^Ne!ASL$SY#AAFi%|lVoUJ=v%6974)O~Oqt zJgmL&MRpJ8cjIzvi=Cf8RYS$5`P}yQeeZDS){@I^nKiJ?YK9LOd0 zIIhg}CNGwYyPBMe50HYdsnQosE}0{t^s>gFn?RXq4ZO%Oog$~VH*_TD5)IyCG%t9HCRkSCqCy_&uP8}*=)-Q$0lIH8{oJGjDxRWZw)crs-3XQbHO!^4o7EVQ1 zT`*A{=k1+mXh9RKBwv`gDdGuNr0uq}k#TW0#~`E-NSG*{=M9-zpKS{a&6qJTV(~>G zb0$o<^5C4g15C<}t&_Cip|Xmx&V9i02ZS`jjey~bWu?OJKMNGfP!#+KnuZbBCiW4} zgc$j?B;otRA8hK%O& zS&0t^rnTGC`o|B0*W#RG9G;v{rIW7sJadzG4kz=uc<4Y<;v9Cui&me_$LuXWj)3<+ zW3>>lSLxs^3I+07-blKmK?INSWy5jJXtyCG4uhYz8_l{D86I--zHw{1IN2dGZIZY$ z>zZf-9pT9ICr#TMzl7;CmJp?6URqqG4*&QmFSRTG`WOF${_>w&O1x&vl(7zER-LqG~_!G;xTI;4V&E1@^FOGqIG6b?Z zQ1RQ1F$tz3R53~`UDB}84(!4fJ7hHj`wLX3t)>zb!|zH&QAiK;gv^Ik^+laE1047! zIPcSNYEiX2z>{qG%s!H*zTta7_D$V-2+(6;{QDn&T3_%1x!^5C143uTv4h zAThx_%G}p{k71nLw)~914H$pY!5BM(AU*X-P2xcF4W(iciSSs9WAUx?#3EA~5b7m3b+syIky9{*%%nEiJU1*rad^5lL? zseq*;j)q8g5DuwyE5GV{q#jNF$Q5H(DL$O_@#eeQsTE+!i6q44DgK-L#7~)F1fuX* zMtKr#g?2R1>j$!aj*k|#Hb6clZ2rbtzn;_;e`E2pND&dO2xZH_v@Y&%j2Z!@qNoZV z?hR(G8@dEiB%gT7*TuD#p6>M90icw%q1o1b+1y(Ij&T#wPQtAm0E?U+E*C zIpY?~m6HDcU{r$9u}vkvR8j}WDVI*Cg!UTaTzMgr@J%kzUf?W!SpJzC?Xct^Ug|#a zrNc<^KCw|b#Z>6Di|zor-?M!&NMcuwrYRcBO3L%ne@T$xCAE5EyrngpHd2HEnDeW3 z6QR30W)eoj>|C zl*&(tf6XrGZ-dS4H&$m4Z4EEA)=K z+acXjfhr#m{{;26BpD&*-^G9XZ%zC^2eqk@tI@w`EoJX&=4@o*D&k=8;%a2?>hf=F z7purPtcjz(r`C~1y6djE8IQ=n7-q3c(aosu5K9IX)J-&n?8DtLT0ecO8gA@F6GPA) zEmLBhE)RQ;&h#sYKZp>K%A&&TlQR+W-D454*8q$6DE*#lB+mvFSq+Yw9RGuvG$^OyW-v{&O7e z+6pkknpzv%N*HP}i;)nE;zoO{X)bx|zjf{ED}1Qd8zb65H|e%+K1_)wb#eNXjmsRr z=aM!|!5n|B!SV!&2wSI_pP1Y79?lv+IErL%#4mv>H{V#7B3>HQb`d5H|Ka4$J|Bb zx%17b{`Fwu^{I2}&;9F=0)!_l7i6Bi!egpHi`z+a`sa15C5INTrMN;9g4vLv0@Q&| zBCV!Gg^~F;@fASlWCO|iR|9gwljMTcQ3tx5_`_TWThD?ja78u035AU;OE#w>E4U z)L~Ry*J(dl=e>b60u(DW<)Rw$l#7JQGKtNQlEWQ4vM`5z9bd7S8&0<{7;k{3L(^>0 z#-MMD@Jht(p#=|ou%UcXCbA)gh9z?#*x_RF@nTLRb9sf%9`#B-8RNL5e~A6wItZh% z2#++eqkEOJ)@+#FJuem($DJ@F)?4%=UjsX0PuR(z*eJ4FGr+)O$F%e-s9kk7JRdBe zQehoW4vPzVz3K*8InoV(>HC`gvnOasa|hd=qeOLIPAI8K)Ze(44xQkaUyUc9DV}h@ z)Q5sHBV_PV3E^3d_$MgyD|Te6>082y{lgreEpYzr=Mmp`Jl^M3j=X`X@9mbZlNo+4 zP{;VWI-l7?rXM~bC4;qrpoCsGIs0TVx#^ehN86L1G!ewH)|DAcwwr>SV6e zhuFHH4PE|qh&&tw1kZok%fd!3W^z`}&JNB(CazWv_W#w?syhGrz2@EAPbV4!8|stn z_kO7!*l}D6RD@(~bjuuJ?N7zho@J@`hS;?p$CJ1)YB8P{Q%5R_EGB)oS|`Wbf4_S|ZS(V+Jb?$Is-G`sUqM=6d8*EMc9dBatGQ|d zohGnOO;TkWnY?%0+B#}BI|_0F3redNT4RMI13sW>TjhAyxyBm*kk-n?eQ`S!7;%4) zrlW?m zYNm(81iFZcJY7pIba39H1a=8Y7JH)?^fGawhNwy2uP0XWN)-b^0#j4WPG*7;d3gg^ za$hQ>g()V8GEco%zr0rz#3EMO)`5GY9yK3v;4wz5~=6hIa|* za+Pm#Ld#O0H2lu5%iMG3^+NsSWbR4(M!rj{bkS_l5m_gG0W5K)t@8L3K!*!gd>i1JqYO5S>wGzwK`p3#d`jsJ=H zzLqu8bZT9fqfEA_zM*E}7!p-rA2Rd1Hs#hEO#hWNF~M!o2Ka7DA0OGuc-)TgLi z*XP1TBKQ2@V)B@YLa@PI_Aorw&ADE`>|7!*V6H_SDL?0HgjOB?n1T7x@N}j>^O7QY zdsa7Rol{x55l;8puMG>r?I7WfQBie)7sB zSO~v9Jz&cqOVUL1N1fJWgibznY~!g>C3PT~?|g)qVsT;JY~54^_`1=-2J|Oy3S1AZ z{*!vwSMuvx(`YPsi5G$7EuMf6)Ki_gtE1Ls18=i+Koh(B(o;x%6dI|>@(D(2MWGGB zrpG#Z@y}&-A!(e+_dT~+BqHeNZC5#7zZZn0B!+}6Z}>kN!wNZxLL9IO)~Ia}@*2nq z5nJ)E`;fE>Ivi`gcVj=>)2+JeFk^*pulAt*k{o2&`x^?3=u?s-=p|J zgTLq65*ZvInKbkq7iu|kn?felULPGIPAilF+^BGq#qzIJGUF(LUMxF4pRs1NM1#b2 zlltz7+kTt}!aFCei{uH$3D(Be>1yM*@P)pj!VrQ5X9Ek|Qc6&Sqxzk`A_}X8$8coqx&k-P}iq+LDSG2hFW>8heyD zk{B1gl8GG>*P0Oo$FlL&@#unfWko$^=58eqoD`R7$;zJ&k_bq?wk@AIf*O=_pS*uP z80TWy>I!&9*haSnE!FB*Gj#ntS-1MUuPu{WZ#JPRzkMkkC&8>}Cg2G3ECPnFfq}_$ z)N{}MT$-QN)s))QQaTXb*sk~RY_BzlkBF!{ZV>L$H<0&~p7{B6HfhEsvO%;gB9|D) z;?RO&N{c~`5oAke35so@{eM0tjgqUIt@ zG@sM$B9<3tc+A*H+_BQ5)~K-a3BDWc^0)Ic3u|sUce>fCy!=ejHW>dp zmnH-8fuu*#l?jzoCA#OonDCQ#vp-VzG(O5V1pXdi5 znHKpJbYjt`*4e`2w~;B%NBGd=ILc0mw{sJ6rf{a{N7Ueu(jFqUm+!OfTeBqzM^;v( zZOnmMiE7euQ;8d*1(70ZcXN?}o?N;EXnX8EtTP832@8N(&I+a~Cs`9w6IBygla5oa zcc_0RB8My1Qs8eQ{=y!ebMjdfe zIKq?9?k6NO87vV9Iq3)~lZYaKaBq*SM=9g~42}Ig0)0su3giT`bA^9{VF$CT=8?$Z zuxYXR*qM<-DDc}4?h2E>Z>YZh;KZkS;7_b#VEe&~oyW}m?vW^nH{lnxnA11P8ZpWi zH0Zb%s%MGb%XE9w^+tA-7)@7&FEVHXR3oY;W}#7|&TSgC`UjlPUzD6c2Oa4yx>zL_4%>sl%Mm< zNL3>F9?W_vBZ#R$>)`iflL^gOHNgKO?VW=wZMSdH*qx-q72CFL+qP|YI?0M{+a22- z+h)hMo%?-f?|th0zPf+xb8ppCwbr|8{W+dF=a_TM@!sRnim zWVZlwoz;7-k9?_FdjSH>H5FFYTcrI^c~!6Tlfqd31je;*jy;_mCa%;AQ-b=Ta<3HGC!_bSuU3 z&OU9y*bHU!AyL!8W%IkYpV71syBu!itcYP+84Z~$-Tn|vKjZp&g{Zi(0`@ALm|8g} zRu@kY@xiL3KqQo#AasvHCGn&y{0RSN1doBTzG3jyXGj&iuS0NrFL*s-Agwv03<-LsRd8)!o)NHnB4mNe#8dqz7)ll_f%$o~n>Zx*=| zM}L)k2}QR<1_xHQ6$#}{ z*EkDjVW7lG+{{HC6BSQOblR`vp4#oQs>?Gm=3ldLV*<`|7hJTTy5lLCM|xxE4!hc} z?62tm_}tZeecpkCDD3$GTb7nBpbg+HOwGjD>UUH%M?-x2q=^g`TvwOi#2vch)WYp6 z+NpJ@>F7t*YwC-rY1FA4LR7f4S{8B~o$5FkFn!pn3wsRI6aBK5aW~?RsHKd==h@!4 ztA16Kh@Dy;sM2ELNCy8w?gjX|gd~)gp=MMwlS)$Nkbq0~h8?M?P^B;L)yK2JYqQ&% zY3xe(s_qqL#4kn-Vrj8JdIFMmUR13s`kFBe78f*F%u=4wGc;C0r)2pu+}G|K%`+ff zM0aigIk^PfECVxN@kNj@2y!YkkUGmz&yCjOTB|8i*)%<;C0UH0gc!}jMQqVMWR@ux zCbW%RPP0eWE-%d-S-mf)dg`mTEdSirqr-UR3v9e9KtiO7!9As{Zk`dm<>(L9>GtN$diFL{=!JCMlME{%Hf?#cBLBL<3wYhj)_*-AQHh;!e{4Nv zUdGZiNQRvoiEDdl7*kDyV1LR3JS;c!Qj1Bom5Viqt17l?lN9frsZ+6&7Ft#t#;J@b zRASqYs?Na{$Q_@h)@W?Nv0tbYw|&^~;co$Sntg|=qs*+Y9=Ad09TEQ4R*AYHWKb@t zR_zRTAy9~kR$*^7Tos>dWVxlyV6)2?#i@v%nwu`K$eEe1`CE`jdw4e$sjhuvj zZH5N)C$9)*)HNt1{VMTDP5i^j*JHOPAk)t?u|nVNpd-{Mb$=m=@P`4&i`H9QKpU|< ziYDBrLK^@#g?PiKN})`^-})h7Q!1*MYCf{5bQN8Pk{ZcMp`81}F0I@B5ad{hS6XO} z;Twc6PHMAeDwFb-6***Lc=6RC)N&;mdz0TiQl$1+>E9Sbj;J8HB!mYj05NKz_wU-X zs&x8A8m3)EuS^<lsiXrqRDHQs0SF-%%i|hs#kf$22@OV*FZHG}=~{B`4^UFtK7p zA0v^kIIiWU-sph9iuZ_TM3HGBwH?*d0aYWX_~C0Gh{H8}kY8!#(U}6jk^G+d{Ra%O zA<&4#*rR^pjg_%BoX6?GV7b=0r`U2Y3*Y4n(~I&CQvc_SlmcoNG#BNKeJb=*@6vy5 ztW)d`BYaxt-R_l|nZ9BP?R5V{xrT3N<+IP#B|d&ZUmYE%QS%73EE4bK-`C$BuDQV> zu-|P#7$&ex*FBhMsknrC$7gaxUUp<~8$z6hUx;=E-m-H^vFUdQJ>U3j?1s)0z$9g< zCw*HFbl^5VY7`S`mLU(FVS2SDO5w`nkAkT`9LL1}Dl#f7aj7kH1wvTJ0ILp8%=ZYd zV5aNuW=-?My)}#0J8*=y=2+niFqzsB-@^~-2Z(M=Vz<7|J1N3)^MYD+J!2`a1ReJ^ z3O7$I6;!#KLM)2W_`EN-@Ab0e{`4OIVd@T#%uJIiWsqsm@$BW>elg6e9-LS9K(*-b z(cv9+9XnecXzDTIJ-&eTF3eO6C#6GSky8<#!9gR9-ul@mpy;?a$ia8M^l`ENt^yH| zZ$Y8^6d$Z1nEeAY+aQB~-jSei`)h2A0{nq;cb>H0?^G3|ul4sBlhw1N$ZG@395;3+9m~zv z&G(6P@4h%a=bUXJ9$7y+Th1IvbiaCuhA!uwdT@SB#~DyKR_ncl;Y*W|R?k%Cvy z3e!A(CHSAE1G!U*z}8GD|muX6!$27(56b4IT-Mz<>l3;RSD zdRkQWP8PKi9U2~2Tt1Iml^=VX4n5$l(Qd%p*qCN@|B)-_LaK9be>Ml!TNbZ=E4&Ph zC%NGqJ1#dNe+|u_K0h&@+Bj^^;R{ciboBk3dg&A<_K}{1D9l5k~YbdxYme|6mA zGf;&)E;q`4K~}j2pMB@6qMb@UN;$d{s4zqrwJW{AE-=>qnJ-J&C4>WrscH$fY23u* zZaJ$qS#qn{TKG1M{t7-{o%y(hYTq9F&RKx4272YVx%-&&`EVJ?ID%sWShM8rHd}iw zx%SF_^Nkj=awATHBM;>Sw~-bh`f^dxzB66K1o(6FIS98eM z%!XrKoO=qVTO6d7%;4s8VNX<4Sc01lg3-vQysBmOp&Gd|N6CMnx)8lSFw`}oshCT) zc&gCyzt_*^B{HEPEuwtuF^{;y0&;8}m5N-oyUsD8R=2efhz|r$dYHK{id;I6?@q%m z%4j)l*fq$5&dOsNdgBaC$E}{H7{2WkUiG;fQp!E8*2SJ_6)URz#r?3R*AN_GZszV}Pc7{uazIjz^{#7(5+2?BNIalYs-v|}xF63(m8t*!Q*NWx>C zR0b*4K7VF$+B50i1b;0YG%sJ=I~)(pqr=kYr46u3BWU=bx-`&ZciJhDU+TZkEJpr> zZ#wSs5yb9KlQkA(U-cB|w%ANE!>acibD=js4EXKWB5wj_)^^4$a#LR#SF1H4wPJw~ zBGU)Q2CVyBBi*s1mFnX`DD(>=)K4^`;M=ILH+_G=)sF0n}1>$cl`!YdT%(8-m+vmrq@B#T{OmQR5(E0` zkJ^AfffFBmBmYB~PiBfC+-Fu0Ih%zOJ`}5OOz3SvT2XE~$n0ILIVP8Fykmgt5bqgD z8eRYvjH@OJrnKxbIAKgEF^Y`;|q;T8&)?(wiPNydtrXYjO=a1f<-30#pLjF^gU6F%+t5z)4G zYro>n`u6o|+}owsF(zNWw1edcHjo-L7MEbAlZ-+1^ydH~u^MTm%isi1UP6;a4-7&>D9_8uMOO5Ik_tkg5EUykaOF*U z&)M5uiR%v+3#)S7$vl6t+vRx{jHoO?*}-8zCh9o;w3J zXPIM6Zm~*+$vqYo(Hd<`F9&>=3*;s!5EaXz|HXJcEk@)?d{6bo$uE)KF=JS}6f2iQ zISHK>DY?JP1x`U{?TY>jA{8`}>Rv0kfj+u`4~hZfj&9nDD`$`45Z`UsgW5WIA*&JN zZsw%^sr#xi+n&z$nP%X@Bi7%~fmJ#-uGjzT3rRQ=9I8^B0bDIX2(PAXvnL)Cj^X)t zl7re}oTHy(*TBsgQf|uC(Du$=fJ{yn;|l(iYqc`3l6o|%Z*kLTC4u1yz|Aq=Hp<=u zRh!j0uX(_3H??t@Tx}Cus0vyi$sV;Y8|CuR5dAXxht{=`9Yl4A8$1pMJ58s9(Rg1_ z&;Nu+OtH)nH6#cKGQxkiG5EjW@voNQ|3OC8^;A$b0RB@@^)g|t5Z__^p$2__z$&4E z8;3^}2+@iDGPL2y1h2Mbcb$fE<918ae`WDrjMQ&rX?ip%>GxbESUmAb0HI1F_df3VsWRM0yjyHF5 zG&W4>2_0G;%?;iU(v**QU;{)9yLFx9`0ZA+h@_-P&72KWvQ5J~%m@7vS+fdsnj@r? zbc_8`DXvm3h&>3fQYs5Vb{0k16Gs=7Y7);d0@P}=3J!jeVp`@@O%C6s>#8g@B;9wO zIA1Qj%kDSqCGI7*r4F^nH-4*st-Vgj+~N%C)+rG-JD#XY;ahi3XxLzh09qg`Q&qi{ zfy?s4^r=hTuO(EXO-7JJ$vN1Yf=Sn)tZZ1~qUp&urmP98NX^I>#tz}hx{WV)w0&Q0 zocxwOn4E-iHIsH3X_KE4=bT!sJttkkwvKmu_F2n3WT|@ad;{H6VDr>|KUJFmSN!3P zDaFB2v1{@qf3``=pL03u)2BCwY&S~e#=A&Eq)i~y*Fk8DRPY5t;=<>xqgm-t3`V*@ z+2k0xR1JO9ZVj5qD9A-XcJjR|sOXfwyxPO}eKtT?on?}7PPfJ*O_a}A3pmotOKh04 zbGkDkx<)v)?z>tE3s}>$IT(CA%{(UhMxadBEckt4KFQ5clEcbMA~fMtb4R~AYxmzjIjP1pS%J){gwFE*Q9`>`fBc(6n*ruoVq2XWED2kftiE1VErM}T0kUQE?P_(ZUbsL0x=t! z!`^629Oh^zu2jO$rhuNy6onr_M19-|?e0157&GEdi1)KKWF2&4fVDrH{CFOsx5BIq z=Qo=9PtL%^dcae-^{i&o+hlHF*9Sq^AvSlHu0^4xVO1wcHEwVQfZRtXZFqo^nLgc> zC=3FBY~(1+MhR>XgDvYVDP=;tTF1S#!e3R9ar144rWr5*(hai>n*+r}smI@|>=JnN z1oav4c~w}lOEWg?yfj|v3IqKVX<0)VR%6t-$8~IMmD!?>+h5sQwB?-m7e9VIq>iyj zK5m4sG`yo4rt(=tq7Rg}ZWaFn#vi<2m&YL*uMP*a!rN*h_qVNKNWk!8&Cfl|B282z z>Yv4nO2SwccQs}D4VCV z-TL>)Mle@UGtXcbF&Mbo3RmDASkU4NWMG+~EFpayGT`+e&bQ$# zcgI72J&VEqU-YTv9R8mB{;v!zYUSc+S^#JkHN13XH0qI}*|-(qz(SqfQoV~c5Ls0)5U~rI@j+=?Zq>pdtCO>3M$kJn_>=&jtooY^M;-GfNgu@154*6+4X~JJs zl7>X~p=@z)vbNGl@onOAYjS#$=Dv_44#iROxGnD=1rK78!xk~bgU;7IV0%oqcU0rJ z54wqpr6#Lwy6RR>b9U!i;A}ij!AOaZaA}Ao5p@}}B5X3BSNK~wg=FSL07N{j7H?>c z&R8~BsDT!;Oq5OH!ODH_VrGU(SO+oJ<-FO{dk6vc8$1b$uSvlPTdk>ixe1rjlupH? z@L%BUczf}frgN#M>#Z7!x96&*7&0^;()_pgZ)piJ`sdOf$jOugb5d(+1u8_FQa+fo z>sAUavzr&UE{dqyp}MZYChDG zUNw|8GO^3@Q0CJ*z$7yLVh}xj7i`~1J-ZStm6wV8D#x6#7w0uV1Ovq}olFxK4~3-0 zd24Nm2XPEOu*;}I9h*&W1>eK2e=f4wK^;z|;yKP34(PLVV}6$6w(1y-{p5Rv)QH*2 zrJzzn?FH9ALhcAcf)H<%`w3DATLzHHhfYVd4U)IJQT{%01!e`m9$l zr5|acfct?^=pYP@m_@_2Sn)Oa946jVPd3L`ts%i3vIi@>YZ8&PW;>1?%r&+pyMyX` zI_%CILqsjvz@^MNzn^0U!1eb_HFFqZ8e#lY&!6}O&F7mur8-HBg}M+gvoH*#yu^jmarFis@!Fi`8GiGP#Jz#RqP>xUHkI`O8TP zMI|<0c=p~=&&p+2&xsTY-VF!IX9$4YTU9}AJe}ms!aU5i%VyEe!d(QB+}&~6eVOdz z?-*VZ-S*@&3VgtXisKvaT094A9uLPOHxtZ)H2VZAO29sVcwMZRWO@Dw-(cW$>w?63 zbr?B0igTGJdUNCtJ{KnaLF#h11}V%tbgqy;1417noGOw(8?Q?!dYZFM-G4uV>f7r` z(B;YqU^hI>F_h{>n*O|%V+teW8DlBRFKewf#(&QIsfrBijNS`(fu0bhlERIFOhv3D znxJ0vUfd8yZ)*UOSXAHx%0tR}Qp4A~MpLI^oGO|-U?2YZqfNqC&{)y9W}y2A3+c_) zKMK?;Emp|5zqpIqU%#>Zw=q)P%FN^clzjbBKovsH;gj}JU2qOVQ=PekwzFze7K0@t z{lUbJLE3l%k7VxCLMLJO6DZnu;O_2j-tFLS z;U4hy@&XT%)5{w;Jv`hoPB2N{#3Rdj7-7{i(P{OzMdV@i+fV7*H#A?5IaO(JrcFVu zcgwLRiQfrSLdA!hJjJF1CJYLP19-A5OtEF}c_vx+{J$LEp10|=rz*^ z6NoA(^#Tw`Xy0qOMwlV7O&!_bnP6(BJ?RA;Hr_yyq9rH~@BSEF>Y(Ke=J)C$a4+sE zwE~$#@5mFP3EiBLfYgpvV1<-B-jw#L+vG)$KY~Z2`DW0gXj+R6g?sHF6XE9IO3-A# z{xJ_M7pGCc`|J4+%wL7Te*y5nHN?jM>gh3v|LcFM4n{7n|L*hsdjUl4MEP&~Z75Lq zv%y1{l@-HaVO%0)8xs+j=dj)(B~Dv#CF0?5zBRnwAB~mg)tuDm0)ktF4m+)VN=|CK z1~2g*W3z?mhR|5l2a;+$JnNeV9s%>4cf$rf?{M4L>RA4skG4kV9hxaRHhpy_drUN# zI8j>m`IL9Oan*^mt<8B?IV13w@F>-e0#;QpHj@nK?<<}$M}ABI8hQ%&&a@;r6Vu?x z9UAFnob?usIEb`lFD%=5Q$0`q-o*XWnd_P3gO#Umh6t|(I=8e;bay{@FtF0OoDJJz zw2*VuZkuHfS5}54=idVDz(wARKrmi!eaGp(mrTw06f{Ex<@2)|9rIx6tzl0wE=XqP z_quEfijB;$b<{KyU6_4Oo5yDeTV=k45Fr7D(&g?mnfdv^OoovC#jcH#X~tV$)+<>U+#IvFYcf9M=(Ls!8-A#qmxVt4~{h z+91$Lps_|VFHj=_=F~txp;3mf>xiAQcHY;=Rr=jd^9=d+mw(F*+OgOxE$6Vp`hIJn zYQw!KQ8(?L7~WTpP(H0mQLfO6 z44r#X=KuPg9DKRC*(j>CiH^;0ah#smtpHR~@YPAksV$5E9s?273(9%+W}$pTPv~)Y@*lPnesASVlY!Z)i*W8$MVa0l8?F>d>iAcp)!WUW(Eq=C0gtwGdvHH z5AmuV%9KLowZ|LJgy=p3n?(AE??O zzx+c|%~xNy4Fv@Pa)Ym$W|owFo+$2%Z610TCDx&>JJdTg z`5!IrLtWi+NAqT3&@-BtQ(YcEA1`=(rVYN{KPi5odJbG4_XdRrzSUW2r5|}yu>(}h zjfYJll&Z@zY%76VYV6WkOLEmttGkU{6gqlixEhC5)~gZUrbaRbV~435^vv4kis7O@ z{nMWwVYFNf>Og18f3RDJeR+e4s`lFA?g zkI0uXv!_;bkxF<-uqq=PrJj`WolTkBSJPgUso>|)PM~4u__$_zXUxLKooMTeoclR; zCBIx$1jg5aEfO+eXrHLg3{>Aeyf*c=G_~9hflQ>4i5QYIl4`F^F>j-jYqjayRa)-Y z85IVyVtKPyordNbS=evYvZX1_^c=gJlo9zk{+vcQTsNF2dDHP)aLo3Je zgr@Zs9NpSJ58uJJj%0Q~y1c>gd^A!M1Am`7r;+h(=IlR$!22WyhB54p*4i65T#T-7 zCfR#Gnlw;s_}&<<{JF$3?3&yW>?rLBxfE(|F@NNI%eN3+t-I6Gzsn<(%w!q$1ye5C_;&UxkO*UMUg zu8b6xJX#EQgO7lE^FZ1VPB)B>RReo8t>1o-V{vPzxv1tt8r)LH5C&q55X!+wzk>7I zA1Ffq&4D3_Osy2{xSrh1ZC%w(MvYd08{p34w_9&yP;I@Ywwg4)f$_qLF}<0BX)2Xi z?y6DZ;4+9`p)(2}x0)|X6wHYtge#Ij^j-lZ7^bpOjboB)rUf>879jR(;ldF?Qiahw zA-Z?gpFr0+VV)Rlb91PX8B{5HTWel07^VA)lLRG{AqwRP1|a)!j>gSWRhJQ;wHry4 za`9=oFR&MIKgd~(lSh|oSuO|k-Nf$eDvmjbqd7ZjclC*3jJF87-4=UkJOkB zcV8U|ml(paMv#&kF2rD!nE&W z$+vD=%XXxCk1qWgj~%_}6$4Suqbefc45 zIg&`gVObHOVmw2n*N8{YE3g%zxnMWCN$TLR+fP_>{hiElh;?kCX75az!Wm~Fx|Sb^ zJK!bD!kSGmc~FjR*9J6>*M-$xFaf^qd3)WP#Aqj1FaYir8svLQ0%Y0f$*@y}TcckH zRV@(?Q%3Yh-Wi(D5Y}xLYi%1UR-l^tyPq?aD&)h#%C}>-(NBi4#U#+h6iv1FY$g+^ zdnQpXjO>D_aeIio&%rLClm?0Wd%xMeI>qLz9eFe~lAgg~M!(C=f5P&A+vUe+be-V3 zD?{W|3&rx7hPm-i&kv9(oQe%aSZ2@2bKw7D5ak3{fEOBM*{6}z&h+}r%yP7{q1{$P z&U=ePY0<7pHt3JkRasv09jx8#ktM2}`pw;(ZK;sVvcs_*#irFpA=L{_%=7Xp9-2(UL*v}>5>ZNmX4PJh16sM8cDwMl!T@Dw@HfH&e3+gQUb^Gs%8=%^` zH}krF#9S^*6^AgRW&PxcT%r|r9b|V}%^5@tF8xhAre+GaPS>clVrJA5!iTX{U%TAL z4}XF2sKaTvlNTa_fv6Mq`44AoZM$Zl=f4P?DCB=9PyR=K`5*FR)ql&A;s4&BoAceFS=xWv| zLu2=yI-xX~rXhLN#_TQ`amer&RMiZIM==fHFx=Em5sc@WP9fNxW}~U~Dmc)U_JEbH z4pnQsvC`~>XYNlYW@`)#%dv^4w~3vTQ*r2K-I{Wz<$2&Q?iaqkxk*1s%5%MSbR&c& zeMt)yQS~a!NGg?^+EkkbJws?;`j0mu@#S?`V#Z;UiYqZKK1^Q*+FJb0 zFWDTHVZ|$VXCQkC)~391f`-BwiOHt?U)d*{md4twsvucqT*BqV_D^v0%*DHB=2HDf z6Y85v|4jX>Xl~Awxqqh&(!eP?@Qfz(l=UL3iYX2HDScC-KKDkj1DiNW7RO6O048K|Ti3zEV0%pXKtEYZ&eZ!f{ZcoWXW@UEHAbwGkwAvJz zoF2d#O9dPavL4w+d5EB{ffN#Ldm5Jqz`9x?d!fZ_Ge;nhyaa&k7FjTw2ufHX7Xt7F z1!#evzCA?AW<}nuWM>sLrqET@1i21=2iZ&Mm#~YdwEPj(m)G-2N0iTHmKp!W$cPEjuk?C6` zfs+3hrY&N5PO3O{9o6e5sM^oJ65yGnnp07P5VjL5evFzE?y4K3Sbdut0FDApo*IEB ziZY5iiYkgmmNZ_a=N~2kUQLRLv434AP=x=^F6QqGrDA30X#4+KvQ_=3WCJq=s6-3V z6)duDq0pxT#Fz+^3E@y228>l~PfVS`z!TG5&41?EVKVrgFdVxOw7*)u{b0&oqu95B8T!4*u6}^1;T47zKfC>2c zpq|b`Z-4yl;sYx6gKTqV6Foy~w^`(x)rCG;N>6gQC*?O_|EF3Do4*_fXFmuQ6}q-y zgKY7bU6p+eWrbEVUDq$p1F;Z>4r!s9C4`*$$$I;#=%mGr@e5>aKsUjdS%a5>pO)6# zOTV&+OnQmMA7jVokci19^vUgJMK%1>1FlPIm2TOztoxnN+YTpMiIZDQtd=O|^v&^a zi1V>K=Q<2n!eK@M{+yodn(F$% zPgorESQoEHAWxV_P#VW=YCClfTu(ExtSo2e&;w2#@q79EAR4q(c}Gu>5w*1_lSBj= z+Owt<&?C}8y>oRnN5hC1P9Gm{PgN>F%+52{D@(nbA+^pw#>iZ{FUw6`;OTE&!+ z30;ClPZ*~NiS}3%5aKoHgpc!WQ6a^@Ejj_XQAX(iIgX4`{6(hs!bU4kW2P{!{)llO zZuZVP%Wx0a#rc7Y4=5>f zD)d?d5Q@?u#lCq9=G6BRowSN%x~4D=lj?v}?0gF>dDf1(VB|jJexe9-QJ1p{B2kh@ z-7aa@V$T@T1Qs4>)~MLWZRTo5P6w>G)RBDMiHdkW(#MSJ4V1}=EUTXzG4$Ucv@Z}Z;re8mvYuo%%5Eqh=ClK9 zJ|*of8t}iA!DM1yG?e@Jq-WHfqv)1xFT*f+s<7Sghg^ebo~VLLctaG6mT0EJ#fDv1 zA69C$0IWo;dV{&74i?vfL#5AAD4KOu)NZE(F>9mmHjmM@<~JcxOl1gF%i_0?Ytb>j z^XBn6!1@)Wis)>f1EyfEimdiFQ@Vt;Vms;)*#^p0S~!v6pHW_lEEfT#!~ig|;>OCv z+gy1dL?qE;2_jZsDncoW!(~xH7Jev6KZMiv<(YT?CC+8!rDQl?RBMzvFX>^_Tqu!< zk5Dd)T8sVs-A;M?$*J0DX^cMebC_h-)A!SvBV8IdAt%UkGi8;cH@L2g()e~t?T*~t z{#-statxv0nV%ST$bW=sHu+`~&yMv)yuV>a%m%t4*l*v~0P@XqsKh0@o@CbvM$iak zp$bhxQjQ<`7+P1kb;7pD8-$r&Z@Br1LiCl7wfc|FC?PH2>G+D&i0H(nhxV@+BR2;v zCxaSt3qA*2PQHJDbAHDnT2Aqu<{4qSDy-@_I>t`PeePvb#nFd8{!ANMH=UD*jk7?` z`kezCd=m3CV0Q)aNW=_&s7PJF0>g9c==i%=2s#wE^D1w?*9ckIc`12pV z!8i(INs_-peKRZw2-klZ>i_P2i+Q@5*}GUd*#AfIm#k*+ugTzK?l+kMWKqJO!i{V@ zf1uE$h04`sGf@_P&Jo*PkZCy@HH@Bb5~j=J5pes}=KgMpVR-C4ku?C6AR+FF5WEt- z!oI?CH$QGn4hes-)FVA*@g05oA6;@C<$QeP8svg-!UyeqZg$+E{vp-yH0rO7933?^ zV&h7qO_D*M->uea4wXJA5ON=`MW9D;-a0fcb6lsi4YQUarO;?2i|REsvnv~*5h>XR z;Oq?@hi#n6vR)@7z-Y*hyV~rpvf>Ke?{l&6a`Wa$`Qw0^%wUYEl^UALMIw3t1z)Kl zDU@uT9Ph5{gZymK4z~PU_>? zrfm)?c!uq|$pze~ll|YGOCJME3jo7D*h7rHj{PNQj~K0XsheBqL99EE?FagqhY3f{ zHt2;d->p0yigvt$aHdZl-e@X$5x2VOOs&j7d5YjpD-_c#E`3+%;=GrLbw+n#q z&x{e>?d&cy${R)BC6^+_^lviE&jr)(jgtCfuj?j^!^tgVzSnF78@^` zPbh!X^v1~^)H@W(TNFOSwXphz*PL|g735;;^+Tq9X1a7bXZxrW+T<{c7JUNRDA$+X z!r>Z~VO2*cenLxbbADSw_tp^yF-jl-m$^~lIY!C~VBXxkL#99l-z-e*g|>u?3dI+S z78?@L>wTPIXZ}NVK6@wNA0I%6W_clc?ZC?Nh+szzl}q$HhvYQmv;c^E1D;(Z$T~GU z+k++`64-q6sqAYFJ>St<8!Z#-C-_GPjGhBT%GB>SD=1n*sXhq5*4P)Y@PlUr$#!qv z@J6DnR{{nK9~!@h(+?uBXT~Q`s@fg*p$&y!V;qrxeaM+V4R;0fYc+N?1~I@OHeh6F z;OQZ|p;rej_G^Yyf7ymgKrkrrQMl9j1kxjVRu7LY6?_frjrhp4qSOzil zyAD+1U@SwSUY^``SlpPB!U^&*+A)fxXU%YD)4GliL=1cu@-g}`sw7<2JLW%}xImH( zu>WGM`r!ZFmX^7LiJJ?9l=$BtM#i@Pw$ji3muS-Oj59p5sF&`;aULU1A$(!M%2H8H zUXqfy0G$$DZ4uYd#6g+kGptiyG^EV=(85vh9SV_h>~Gm*=uc|8aS< zKi%Lf!z4h3<>iYYB=gjD@b8?5`BqO8c=L!K`DZsP--0R#xPLxNdj6CV+J#J2dlOy( z=ejNSTG`7%<2kG~czCHjCC873!hGo3w{^Xy5U3;VuRd$LL8mhLE>%x)aAWWHBsKY7 z7RayF_jg@3D8B8D%bl$6jYp{T}P2^tm@r z`De4lc`R@C*92BYlC{r`M=G_(RCrN$QaO^yeU+0NnH5oH2%VLjJ?f>yR>UBAO3BTE&|rtMKx{Jx*EnmUW#JrR zI4Mor{4gIpMp!%HZUK|4H**s?+~!kaWznqx~wAq=}#}D^_e)6@*}8!Zztg3J0L>8v-vHX*UVn zEM!DUcFqH~1GX1&JQ)Xqpub4?${ei2BTBs;6SLDhIsQC#Kbdd&em;JJ|B%wP@a$^1 zb380=o4Uxj9zXnLr7^)YkJ#D1cRK?6-iet6xhk>)M-R!x(K@k?(^owTq?lZV^C$+e zoGo?^pLt0*$;vq?Whwgfr;BtJkoLI^yA{~o+J-Bl@Of3oU=EkUA67yDg6dFMg>^Pu zbbA#^kW&w-87PYVOfdQ1d^`7A~3FBmPlml{>j zXPJHEHWdBgA==1oF%EPQVX0mNM*vm_8u4O7>oAtu3~fu(fXgzqK=lJyU|Y~8{+{9T zjB-Z)!+dU-S=2(Wy*N#6mii2{U-`;TyCrF3QqJs~meAXCbAyNy8dgFT7<&^lT==eV z$4UIQC}mC)CHEOGn*YE|$1`e&v&W|u4V(zuI=|mrSp0C&o_OQVhz_hNV5&vBv+Y1D zh9G?As9No|aOS4iWSkQUAkV=tt&B{^rt6U&x`Emlza#CwbKExhVXmb_suqzp;DvzA z848aO;*LIS#Ox~deeG%s)S zPJP)mP^O6Ca~w4Jv$|Y)DYP*VF-oO=kz`cfTw)C=Q4|!Fdf6DoWQ68Ys^T2))DL)> z9F~(RrXNuB$XrCg{yuUuU?Rb7+IsOOwBTz)Dv!*F_%Il!PF=20qDCSdD(>RV+Op>3 zj)eK(r#pKMD{k*fP>8)%PCvwk&GWlEPryy~#4?|*@09fk;h4u^rWeDx(5Kd4C??2H z0!$lM9>JtIa2Dmk>23+(i{&8Z9q!K{+93d33>MLuRmH$G{ zg}Tq(4Ag`bht*>x))MCMmM$V}f-1Sa(i15ikP25pDJ6R)oszu&r?tvu`j@QLU-zj8 z{J(n}pR0qLi6w)GgPq;q!}Pztix}D38XK9|{Bsk$N_kUZUJ*^8#muH02B!ebUjzR) zBX?E4NTdzj63U zd|QvIwzp>EpL9VO%?>3LpL0kg04Qcq7CFW>@lG)9W5V-5eqSufCC{7#54ChB$#8TZ zBZ^P?#T=oeMY_wpGZ;L8QJ8SK0Phq+6_>k7Jko3I2W_BlF<$u(_|h@7hw_3mh{oE& zJ$p%N1hAs$8Jrw%vBq~Zuqu6zvAGg9nem7A6!Q{bxHiX|#j9(L&RoQPx{>tp*SMdUp>5c=&G7t=7CjFdGmpgIrVkc7o6(c6*8mva*P|(fG$O{=N7m-#|bF{{P_lKYIO5l?C;` z*x-kuh%R9sW!`an0X-o-p|+UKqL^zkBsGDWZ84Egpd|b1sGMS{%J9)A=*9C|`qX`I zsQ=kQ`qE7?%SY^~$hu1dQ{GPlq)eZwmuw&B?uLM`w>JV1pdpK|+tX$8o`ET*t4YZE z!MWp-<^=v2wtTU*cDn_)yTKrpOG3xSZu>R_{aL}Z=HXDpH4QOOXPM4Vf-&{fS8DI3 z=_K|T{Kc2;9}8}=0zC|J3B8c>%w0UeznnGVyo6(L@p*lJ3A`194duymb=zDk&F>;m z94q_B>l<>qclYV_dtB|ib)0jXb1{t3cun6#ANcl`tK#}-&p~e1o6=H|0va=4D{@*~ zeW(QL920T$;oJ}=O=w~)(%riU__}`iyh;Hz&Di9zjBewN4QWW*?WC)7%WgqT?i4P+ zmDe?=VbAAmsF+Cq8lrR~jkEYUMdwPLuS;EWEK3Ewma{Q#3T^UXn`Smmmg}!0!9h=z zvG)D!kFyhx(n*+gSlLjRHL)~&*ow3ttj+b0IX=1dy9m;|1ku^YCExbBVzO{)WQ^3K^wmm*9{Zwa zoJEQ!ZWQV4lreHNvC4o~zfJ!1Lm^yJGd5pDlYB<|OW9R|h(l=33n}wll3nmJVyzsf&Bb*v#=t>Pt4$ zuzm)soe7tq6f*NeQuXLhGwg+RxdeQtcgV=lgeA7oGAjlrj--|z+SEglUqZ%bo@%#b z7RfRer&VluT3{+ET8Bqv@-40{6LB+}WW9{n%~pAVdfYM^l&Lx-E|9~Ivg;nc^+~iY ze`LJ1NpamaG8k+M(yhzTO)b~hDwTkz@Xmp&NjA*76^H z9!3EzTn61F!K_yr2x6?N*>;r@JMYr@XXJI&BFBvUFTG5T5Ej)8r&4mMJ>2HyJz6-c z^Eg$MBP@4&#pP;dg9X2FlQjSJ@KsB;sHsWxVf|0+tn-}0ZRcRO>EhX-0%?C;W6stR zP80YZZ8zQ6REmm?Z~S3L4mh2skb~GJtxN}~aryoYqK7FbbQZ#~EDPKIA?rCKD>13z z=cckedvuw^d+}gTh-MgmJb;~@38)(yKOCltPdUg(J{9qLYiwWO z2pWHBl}SXUgmt~)n+g@CVcCpQ%KB^elgv}rPuz>rjV#+(AfZwF#pDt{Ww`J~CF6mT zPV)U}V>jVfSy5y&oks~hMCrw>|6nPY`tvU;*}mzu1yG?Y80=EgXyoHn`b0TZo|ql* z45`FpKn!YtF`j_z`>5MH)$8mC@}@L%hNSlvX?67eJ*a5=(VB?wJ*v3>;Bl<;O4I*C z+B-&9)@_U0aVoZLJE<5I+qP}nS+Q*!72CFLt74;)+Y}e$6&lTR;1F z`sj1?@pMn^-XZy4$jD`d4IR9%7+hK?V;#@&QGTl#;^2d4IX)d+egj94u?p?W%(sYY zpRmhA(7c(5et=;!`A?YNkmpQISC_Zz8w5tD*j{04xejdZ+!7m?WGT()_@D2{eB$e!xr0*6@Ww(6jQV42Z;(A75Xzd}G_*J_K zqkeLnf4Ppk-oV-Drb%9{VgO=`4l4Iw8X>10x~h3;(~Sqpii(tmHqX%Hf}Mr20Qe zM=}QX_7=8gq&9zl{#|MJ&-o`)s#|I(VyIs-g!F@DeQzOX(M)LHh$0CkLll_=WM1pR z8k1Dp*Q683-s7*V1=f{fXFegjAIp83cng|y798JJ`AcRlBTL-`1(GOVZsq%@r>7^o zp56T1j;B9UwUU2S*sZawRe7M_XNnvtXud(tDvp2ny!OHZ-jb}Ts7XASGv zI?C8*(3%%0G1zxej;Dlak6V=N-ilN0SXdOx3Z0)`F1bH*wk=<7#HCV87z{JQ*@c*< zw9<(4PCbyeqhPcX@38wbfz8XVExnc*I0CFli;hSAuU0FDbh<8T$}+ND@JK2ntQ8)! z>UHH4V@x=oqAn=d7LvN9&5IKZP70Ubyu?#$glDMA%+&E1xNM4=14l^x%XPvDGf0kH6#wDN4W=a6X) zOaRor*|qQZ%jwl-r=T8I{*Hl+>jx7$6|sgc!HltNpAHE_mVhb9YvbXz~rM_zfZGiJ(#MVM_b&H)j!U7FB;~m-W%uT(*#=A zhsTD{DQ`ErwSbm~BEWu^^Vl_FUwC;*levDv^##vrSD9`(tR^JdZUW};jbL0iKL00r zX@`_}satS$udk|W7g&3sA|mG@8Gh=Ma{{z6V7M{9h3#j8-jhOoSynZc2Lxsy)yA>y z{9^E~vNRrMoy!7}^Ho_V6$fhL@fq`3%ZrN;CMlEgMvPh=86q)_vIdDimnGE#a6WnZ zD2<)ufilpm1;7=^`{uL~{P6)a!_dhIuktkfXTi$O>o*^Mq0i*A+dbS2(o#}N5nM%j z5yiTZ7x#T&z%j&iep~S;Bp4U|L1Jzm-w(^H-Bl9lPozW=gxPycmbx~`Q@o#gL-Jx5 zYTgQ=FR?cx*I$7@uO}qoq!#f7w|dNdBSgV{!Bo7{eMdY|0uH6f4QNIpnXMKTd76bEQ3VBrG+vQ8sn@TBSC^fH6Iw_fYt>J zlGEw@Tz=YLs%dXvjzg6^BcZcdFZr4D5TC+ovwke(B+Gf6T?2Y+#x+hWz1G*iF!_o^xTI zXQ9rkUUrzU+e#fU42Mi_*(s*S`aqU5b2m8WF|{x1M%*%+xa7!NQtR+iYx?jERmCm8 zh(SKKn`M!{?UF!?Lr-x}%ITi>)f>M(2!|E*Bi;3`#x^P=rSFECs`$hLWUFpLV_@n2 z>ao$(OnYRq$itNClGDS7iTA>|A;w2{b|Awfyh+-!Ez5#Iyv)qK&6EbY0G6&a6?caD z@}X$Y0&8jk0*iR(oCCp_CwNST!*(d7bey-YqY##mAFV;3Bd%a5da5OO? zOk0e2j$OVd#*=e*K`yy0wwbqmiQvpcDDyypt)6UN3nTR?OjAyI;J|Ax3>!~v$rQW# zI<^A(u}&%8K|=-nM43i`jtC8*9FC|dzD$O130pB!$U;lJ2~v@nmNLW2YIF_Vo~vv} zHY@u)#=jx)O-F?GF$V^di%yv-K^$&e|zbP1Ql_% zXbSy9p*ALsnW<%QBHD66*@?)r`1Dv1Q%XISKl%ydiL9Nk*&c@q~)D~xvFCl zgKcFrN^@ETP$TJjERmEm>9w9^A|0xq@g<*Z#cOBml;WU>Ap%$|$dF@l=5uMTw=jeO zxG~GGWp3(aD=IbDl8Fq7j?l-EOsim15AB8E3N;wXVXnw=KlO7iEMxdkF$MmJYChYn zH92>FSb^~z98)3%nJ!V8rj#*j1@8Hf=w>7s%?PeCt_r&?YOwxhX1H9DC-SX?j>QO8 z!B=}}6IQ6}nBE@HO>M+)8BQ1nJ!C9oZo67XW$!^Dmx9dEf!Q(~sr|YJetps6o11B~D>R6Lqf+*e9M=Vw!t#mRR!cGJN$JuTok1 zDXKk)15Lj>C2B1cYHyQbkl)O#7iHlwAB9>Vo9ypptNV=Wrs$LkyV8SJq%UXetzhkw zDG0A^>Q1F01rHPCR=MOoe55(CIaHiFH1X3%b2n3cCo2&66c!JypF4@Y8n#P+Xjy5` zvbm8*2c}Y@Lvg7@*^z`WgnyXL?qAg&@qqEJo29I1>8U zfZ2N4d26>bk$yelM1W#Tbo$F+C+9?(xl?5cPS1&{*iw|@Z1vvC4S>-M7P`$hwsw2M zr%t@mT|fxgg2bI4t|WrRDj&oWKr#Vc6Dau}jxC+UV??U0cH>$NkhJxJ(o?Y)!sJr6 z*{|Z0f0`LTEq_z0oeZgGPYC|Wf;q$Q7mf1<2i|fk15mSkpcxeuF3n@-)(uCHt zLhi2Iordv1>pUg(*6Z`?S7hbn;--P0k*_9K)5}Ko9kdt5tfD*SSH0o$6^z|0x>iWu z`7psp+ulI-t=R>QuMJ{brSugPZ=JAb)Y#DUE#DRT9kJ&=1s`UUdV>Q_0L4whwi1|t zfC<&3WG^>(vId>U>8|ysdH1OUo>V97^4`Ug#>tz%z z+9{};dcE{6iA&?4U`!?-_ZBORTV%ebVql z67)z=u*trS&}Hc$Cp`GRU3T~4q_kTF&C(Vc|0mGEEs~|`O~^ZDM{GD(`q%}qZf<&O z?Aapd|Qt;$#dU(+w%w&w_brsVk5HhIAme%m(P4-S7};3nXA3 zU-;v7L&4VAOr5M77b zW__$w-B`q`T*NAWk|DQ{bs~R>okSeErb4&>(?p(J8BMj&FMaWgqIIlntKIy5?2@2!s4MHdDcagV^q zWOqAbhy@z4i&2c==QgAo5nz-&ITdlVbF_1Ma=_acaeDel6NvGT{D=|44dsma_p@9- z!5{O&AV5G0p#QzEr~jDE@&9jwmGY}4Zu~zs*dS$@#xhp_48bq}Xu9p+WUv>~|Dg=l z9)W&fNMgkBP6Y}@^emOq}fuN}6ClXxDfj@*OT?L_MSIr1u$7Bi>; ztI0@@CpAFdT|h!N5&9+mP7vw+@@4d(0!gfX?@V-pMPXy-nL2(RKvQU^mOva~%z|k+ zigyG53{vydsrCrEmkB!$`}(MA3{u_+XW$-$qib5*A0xxwkDO?rPY2zVI94I9YZQ<_ z|0{-jx#&o}O?RcPK0MLIbIbZ`Fz3QWN;RH$%DDQov@yBCvXIkjof^LDEZ3S0X?^7@ zOO{NL3~=}6M-ZtA9>mMiF~!*Yba820{buN5_3ZL!Vv=m&ebBOeJidN)c5Qg`S*H$> ziUs&mHWueklFS$(NgEChf~{esoy^+W`$6$g#7wNpbE7Jp*aFHD7qQVDz7L1rBCMUW z)%W24R4kzS^F$Lk#Vs z#IZM8A?60~!YENsP~&jxFnBanbR_7>Jj1^Rs7?#cip`$@Z3F$!Bs~8Xp#Ky!>HHOz zybV%4%fxhn&A$5)n%_+FBl-lRG?lRh;L^y0A(^*i4bJ{@ciWJP_BZekQ0PS?kY-@^ z7xIt0w|Kuyk~^6_Wwo7bbF%n-e*T8rWg5onzPSE5?aQ3GdG&3Lxw2&IJB9SdvnyU| zXbi(d&JJ=gWIviV6md%%^~Og3RoSSLY15Pr8TsT^b^VCjglrWP%YcXp*EY^%&e2E$ z>8i6f#U@D!4T#9yAWue>G+#P1e`z0P6dOOZ_3gR?c#w#MY9{n*cOwtdszR-q!y2@A zsY$u&2=vG3cC%%-GEr*2WPU7B%RtAbW7}r^Hk(^8UP+wsD1+PYS(lnXVVYa zl?Gg_2m!|xO+*{~HiMt)h->)-RX2Ggfo*l7w9YF6z^3Q}l%{dLltNz`dT>{whoXAX z_~=5-^u^yS>D44D0sEGuLL(?}Vw^H+opJLBUf1eGa$HpH+HgbaDXJpcBXDY0NH_}=TH9_ zri{wqQm7egfu93q2go8^5Fwvj^hWxi(nO)ZA&{Yipw&LM$0f-!Iq#ZLBA%* z=Zn`xAh#i7FN4zRUKeDuQCDE!E{1+a@jcd&DWWu4n%t%ls60+*2n$b1sasrSb30w7 zdtFU-e?HIn0Zr9b!Bq!L10&M&^^MN_d@&M`E9h^1Acu`EfMNYBW}q2RAG{3^PMtA6^YJ&+QY_N3Kl$*X320k1m})UzJmz5mEq`RKK&Ko_wumF zZ#4Q7?lcpjXPc+X1#od0eY9jpz=l<#by7~&unVr+IT}Zu%ucI4M1}%UGdB$U>?O#& zD0Sq-sMQ)pBQ$Hk64f{zrn9q-V!JuI|DDR|gOLykL+a>@gw`9-L4K8&G=zqnvDnZ( zb@Rg8MUyM$^+7uank%*DPH^9G9ev&&8eutKbpDwqg$|9WU|C8V1#h~59ucIGZR^ta zn8=4inbzn$)ms)&5JGKR;T}Nr;U8g$Y3a!g9P5QNHInlXxcVysPR>)STOB@-QTEfP zja6C#g7@fQK8*{Tkl*t>a{Pq85{57sUS^AWO(a4zPVi1BYNz83dBrdEt-Bz9PYHGp z!gl^ZxfjI4vz#&3;TJ;EJLVT%AAI~(=;2gg~vFJNuxX@iV+bjD<&r1X;736$bd zlBi_E|Go@Ca>kH_Ab^12;Ql*lMAZ7PFYk_KPX9a`-f%)u!Tid0X77+$aK*{kZiS?_ zSV$=G7u2$96N#XsT#^JHvTsXqGEs1ul=u;l3 zrCD#i(4t-1bBNA)%WLbTWRFarR?t)13lmwOja;73xq#lu(8Ri<$DH~`cXrSVoQQB- zRO4npq}xs!+K^)NswSbhmqYkWy4K=me|-;Hj^M%wSWVJ;g66SX%Oa8zUE_q7x6if? z_X??ZW3%ECnQ4m9P|%)yvu&%5o7zZW@mtNrz($2QNYG$vXg-@Xlbg}q2iFry$~Q5b zdz4_{9`k81e>-LrDpD>l^wD>m#atz>+ip6v_LpZGQNP`O3{Wp3L5{?Ht{FMS(ASw} zE07V!>2H&5j4?+kKYO-2#e1$n5QSKczY<;ya~6R(52Geh2^S5wOxXV7l3b?v6>Unz zd%G)&XHW>yB3_M1q3STbDx4O0>RE(MC@@Y99S9~46TLbnBTBP(m}lGQLWO7TpY3k1 z)NVsz3u)lWZ0RfsSD*u2&Hf!_i`c|Wv%-PWARB$H`1DaUDs5cTRH#>Zpg3f*bxvH` zhOS}Eyz@)PxpMQ@55VT2QqK2m!qJoy%yx3s2Ws@5gluP%BmAHCBdH^kV_rTX8@2(G z*Vb@31{_Jg^=FA>uii7PL0mH*2dF+$?Pi590ocWe6kH#ak?qcMzI2K6;bY$&@R%o; zWFf35-YqHKv62EioRf*h8B)v9sB(TW*vI}T71iO8+!~2%)ozB4RBBc^6f0Z!nWh`B zxsSwh?WvM;)uGfah9mtwafu|2>uG$b_O)D$^5`_|Nd2=cMipRVpr%uiRG)I-G{bax z(MqE8tx^Ck-9#KBb%<((U#0m{O=gDHLGQSQg7@}X6|ZJb`5yBsJ%%K-4-=wRPE4!T zZt#5Mp2^&uC`Og^SN^^{u8j&St{C?lsV7rR47F#ttETP36>%=hMDxsz&s+Jg%Qb3~ z-?u7HJVNDKGn!>`cr-Cv9N2|2-00R%oJXfJ0v-ZEn@_S-SZ3zM1%4N`nAmC`MIKio zcqq=(Ukt!X160(EiTV6pvRFLBT#C&X#LtvlYPXuOr4E4n4=71&-O_fp)Rfcc_tdW? z5|8>Xi%zTooV*;Bch{RAnoFEx-ZIP#@&=EW>RsC6PtBc(cR24}Vi_4$(q^NLgfg+V z*5k!z+PGcrA_XD4QNstTK~nWZr*Kxj#yKrIiQ7HMI>3IW9PA2)r}DHOeztKIqekk4E~-3+jRb7fTW5 z;Q_OqR_C1;-}+=68>K z;tt2p{S4m&)y(LH|Eh>!xuIp7PG@wI5RAfSxvO>$D`e7EbnN_XabB1u}05v3y%NF%x-->5;f)iu6s5et(LLwUfl6r zmrBPWzZGXL50%SmVppw8x zoXI44gbI)7>Z!)?Z|17=H9yQOAJX2rUNSZZOv5n81fr*>Y3*;nH-?`=_If+E9%a#D zw}+0R1^WYRgYqp?32aJHUVWNUovo5b9t7IZkdUGifW>S{541J(WfItbRD&yJmi$Op8cDuuJe_x{vWQzSGYZP7i>$nl4(%7 z2-{9gXbx@KHoPh{fRy zS7QQ<%BKlxr9i$#o${o<8P@LFO9|r3JUUjMOSX)~d+0B1 z6AUcvit0~|PS>WR_JoU$Niq*R=<+AUFH7V1TnudOevCWWX>Kz+69ialu@hhh!$sTY>MwRh~iPSGu zyRG;jZOEVp4Vn{?1s$Lyxh@x3aP%l%+!(XLg{QeH8)xls#3;vDh+kqlB9@#kDjjLH zVwsH?IdM$bpQb3!ZU86Reb+5sO+oFen83RI6p`!56}2qhSjzHoyS-r|;e4f389;D5*%2Ci`2_ordCi zr%{s#)zLPQzT=6^HNQ!s^wAp|^u%=A;tn`4M$K73VATUn>z|n65}dDlhtq%O_bq;b zFJ*DR2R6e;$nt_YNzA6kq(~PUVSn%yiLs~e{a~Ly`YyY7&Eb3xoqb<)O`HRY8Jw+x zM;h4(pSbSb#!W%7AVt!&Sl0*gfeU5syHNI?kQi7cU1kws%*V&!6u1hliV6p*C?!EqOdVcbMnbW3Q=_% zN13B<;GJK7-LZiy()BZ4*2r!i7>v}+A0O}m3Bq7ot>(P`SnvV4KCY?xfZ(0a-dK3KpS?!-l{|@%w$pJEdcyA&4r#dll6gF`- zF><#1|4inpR+9Sbf=!MKzU&r=1l4Rx3ssqjOf~>L8J(zDR+BcNWV}%Dylg6epJ5kJTfCNj{ch^&+WGl;@QWQNEeH~9n*diC zUH2E{A2S_xDLAW@N{AG<(T!V9iM&F#4YXVLh0IL|A?^H3kQxU+g+$HBgXU!m$C;uBkXy6A>!06#3M1XpCt+pN+ zV-~|oY9x3^idKu2YPo#P%8e7+gjMB}XR&;B!9FdA(&W;(%Y9pyX4&$2a-a>m(^5r~ zA==$jx!tj)s>f8bU!}_1k_X#YKVP>eSkCYJIptPa{oMy|uzk6<-M$8*R&WyRGW*Ql zi^DOW)wN%k+F+9yi)?pQB6WFZP*^Wpwd&z-HpdJyHs**hD23#@4j@7)h?DF()MSZZ z!_+!RH}D&~3GCV_DE%;Reo4Wo$K{B8(A}%R@bCDdRN4-Ah+Z$?SKjciq(9!SI5jy- z9{J#q(gZ4rkb-dRQHBZUe_{u`x1uA-5wg@mA;GZ1K+GBDn$xS>BhlM{#K#3MlEX@3 zrr2xrhXvF4oLKJYCNfeRNcX?U{o4fC!@dNJKkw_1KtQtpqh%px;^ge&Xd?dS-`4-z z?4Y70rzM8?xeT$McVd{$OD?QJ zlJhpJr|?+*Gafk%yIh3lA^)okp_SppeUs?HT|PIJv%-7eq`q_4-p!z6eL~8TE0wTBRpcIjb4Gc>?S-1nR9=%Y( z+E?q^xP5A~k!cePUeL|A)B1H<>d&xVj~U%O zPag*@9K;%m-X7x%-$-?bI$(J9)y}z9%;NU6PYRzFql7{~FqHb(28dgHMzOmRdcz&g zcL6*}4tVN{k4+@$F}~1g@EsW{$?y%{ji|ynN$s%yirqWL=|CHNhsDs1Mi}a=#B_|C z&+v_eL7Ac(_})2#*^Yd&%@-^S_%toHxmM3G@ak`BD>t#3VY9Y4We)oRkcw_;x^SOx zS?C)+j;@WY_HD?O4`j3_2dWT%q*I1)5w;5OjQiS$b&cmrx`j; z*^p2EF1RlNh{xqM?$x5%=|t%q6dPv3GM3OoAoUUMx2W|RoL|wCRK__(hnyplo(16z zh+JM6&x>pC%5W#IEj9G&-;5wCm4&h1P#GK1qUxuXaEfB&Zz%UYbhVB$D-Oi!o9Q;A z#)?rB>Sv=)+KJ%iXI7gJ*t#W=@swA7EEN>uRZLnO6}mct>#+V@xMJ4Kdvh7FcVK^>x*iEMy^A{xhyyR zj`Q^Kl5tEsCRAjx;m;pwkA6KXo}ZV`8wum?1}aS)5d#-YW{8YI#zjUi;Vj`S^&tKr z`5@8x{BH{K2x&X;@IUwgMfmTWjs6!1`bSNss`aHyenw{f|BNO;J`oFa6; z^;?5qn@b9)T;FlKj>ci$oDKMK9>n))I zPQ_8jbz*@jp?!b29Ro2<;QbDd=&k3gfp-XBn_-~?I2szmo0 ze6pihiU=V0Q?s93c+?L+-KLuxmy3un91W;i7xjMO{!?waJvJO03%%aBSe^m|NayAiFI1vV&G?E-{jIBs=F z!TKsPSJ2Yfosgd^Wz_g?&6tacIj4AC8oAKU__sH8{LE8~Et9OV*ziHnYk)90H($!= zh*DR&bUlCi>4e?%LD$;hs6Ag#$b?j9VEeReMCf!0gM+WP|D?`7*tp4;hKkyy6G+pP z4-uGs0@oNo3Vm-fJ{b8JF_%+U&cjnUNQ-Pgt%PJ zwKvBm2{tt2%px0cHu9dx8{7c^6R(5%VYGR8N)h*sD9{%e&b%rZAqX`I?`Td@zJv_K z_~r7%@GIVPQ)EMe4_kP9iC*-RVcESiBOY2IX?_ulSSYKOz=p@V5F}%B4RfitS=|nu zAo)jAIchXGnM^w26eaqIxMLJlCt^PbJv4`Z5bgo8&_JmNVs9iYuu}Zbi45Z1N}eMI z61frbF=nEco8YZgrwN{crY&5pU-bKAdH8Y;ZpN0?W+qcqihzUo#e?zE8JULGe641U z{o#cK&X)9&1tQ#`D1Syx<XRuVy!2a+udml9*H*g~MP=oJM z4pnyd6VrXv6nH{Lr0LREr=BM07Cw+a?}WXh^T(kEhjnCuagc8yx+j==#I}|SAT2bi zS6of7iDL(FExSl}Azz3>KjzGQ+4#saL0+JO1Uejf4#~kgk8Dp%WhcgC+96NK?X{JT zIDz?3mTt*?iFUtS`;UPLd@q>W-_P8OZ57E^upq9wFXJCIK77q4@O znhC2?C`*y~{;a`iPBcTe{KhVTp@4?%;W=LwUYf+xoH_!JjkSG@fyrfox6yo6@aW)d zSCd<8*Hd`1ClfNPxt*QOrjo>>SeQ2_$K`x7_qEa((GTNGf*)~wb+8y3cBp8@ktwy3 zqa~!*_`wIUt{UeZyOVB62DtIoxTdstHQ(I0= zc9et06Cs5EZYm#n{Cg=i?o&R31MA3@(G#e)&OoPn>mBeUTT<3o!2#+I1uxe}br_pj zsYbs>kqyhA)qA4v(9o5w>D+EOezs5B0n50ZLCdCdevqFgzoWmu%k1ccDXjYrdxdmr z3|l63w%11Sb4S_N!aAMRnc*lp#_3zTD#nsh{6UIgQ^TXeIEqK4-4mK5Z85esEG= z=TVOrUmvJx)xj4!;*BiVUFUuSv-iN;`VtHy2kvZ(nGS*@P?}fWjY-tSmjofOV4c_M z4K1ZgnAVE?yq;Gl9VDEip-_kyrVOWkQwo^1lS5uw<%Tl-Av{HzQ!j6oKi$s<^N6l@ z<(2}6j!nm^@i*_HX{h^XzX6OGt#_XWfR4u||LET&`rECK|uLCdLn~+QxGs zmPuXx&9(H)O%2WY#hXK+Tp1Lf3!w$b4DaTbPnVD?7yy&IHgo_-nE6$#K zAJgMl{@hQ9T@F5@m#69MHYL3ibeYQWEy*kAY0YuGDGl9fTW(W*uPghhO8bQO&ApP{ zB5kIj+NS>M3iCzbtA~WMZ@#Q*diXWzj|N(A91i`5#Z2LkayA=MsfYG+t{3{ztd*M> zhdHbxqLL&OVop2e`mk@ED>rn$W3C(n99URVsJ(aEg~L9Tdq1&Cv~GDj48tUszI6hc z47E?1+6NdknYRX}jK<&RRglamyV_vTBmfuEOE9<_Y^EOis*}Au19^+8MvLs^;p4p( zq;KOXEeiY8MzC@%%aQH#g~4CdE;?gH&LUFtYxAARP;%8CjdS_D9hHyP)u~Ae-{-dT zEhI(HlthhYy5*?TU!186$2X2n+>939nXzxdwt(Z);yFY*b?2Gsd~Wf-XLSGm-GPbK zoX{BUF6zyFi#0Z5;`?$1|7#^)!}L%v#0)P>f!a!qGkoSgxGYG8A;ohG97>-=T!sq z9f0QXTYjkAJGvBK{q9hMC6C6~i>h_d8#R2eqE&L%IMjo9!dX8_T_&Y;$- zJdKk>aNxRaoEJVhTbA;Cjj^HI4jWoC!o_+44~iA{j|x5_StSfWR$L|MaUwWJfz_jg z=KJmD|~02kKi|otNQNndyl_bvp-5!ST$Qwyi`?sEO;9x zF`w(HkV4QnKR7J-;YF$F%BL&iiBr*y2wyep4J{m}9uP_tJf8tN-R+|BgB zjf@Z%W`i2q9M>vnI{&ckLlt3n;VdO0y`(!!YNQ@UPF4Djj>bTv80ITkD&ZK-TCNqA zoS!-;nB)o&x5Bb9?*S)|`|*U%75w`WEwH|dGB)l`*UPL4>+rz0#sF zx*TeAW@J5iSga8KIO29$Ealj0&FxzIBg5qi#!)UKkwTI}HukW4u^00}M78KQ#AH-HK4~J&35SdB7%roh2#c%Im01(~ z+4!VFcXzdhX$G{x5aO6LNKq?RVJVH28niOu)Jf|~G^{d;nP;n8YcbiD?6=*{QnIw9 zS|e#S-4tEC{ZfV$4&C}RZ?)IEtpt_<#s)X5%ot_YxVZI6M^QrfObK(v<`ho{Mka6E zTo}bEe%N5tAl3e3LW;#%v|B^`!(@YS(w?|2CkCU_Xsd8($CFTF zRa1FG4(^uN2&HJ*4+Jj_%|Gy~4?8h~k%A3Z`8)hfEr<^f|AHU2!yowJ{DGfF&~VHj z_znMoAHdNjg8UEsv|#_h&*U`CPPCT8=XJr{SE2`p;W2KLVrI1lFUyD{y+>o)=Kb<9 z!=MBBS!aQ%WG0vWgZh~QM#|#wCIuc1)WUgx3KiL1%QM5rMh+Iz zZQ>Bh$Uz9K^ZSvzs)|Se(N-CxYxK-Uj8P;jV79t2?BMPthzrj%kg%Q^H2D|`R`29_ zNm6pRxy=?k=Bx%=E!ED6CXM#OiDO#H?fHl}S~tnKonm0^TV-zQ`vPqCYyS?hEj&6oqD9saC!X02{y=`Jzp{2-8_O>j zPY0}+(fZEKEz}Yh?ZJ0sjz`N~NMw%0VTC9u+wWD1>G_uog`6`wDoi__mPf2__9`Q2 z-lMb0U#T%Wr26-&P4o~C;c4u@RwhkeSzBh-=zHeN=$!ZyiY96%KBz`e0~e+)G|XEa zO;fE6516Z&wc^H2Ew`oj8G_+k7Vez^=! z{C|UA^dIbS0k^yZ5rIYaBXAHdeP95w-{f5@kVqwrSQ8L_I z2ZTQN{FWmMGh4KcvNT2x30=>d_qQD2tVEWnAZ&K|dSMN)wovf-zSi%K4UusHGGg!uD>bvz-T5uAFkSxsh@e z)iV2$a&e2((XTXXWZ+oGI&#M@x!oMQX**_-5>0aVBj4+N)e^*^FMM$xsQSul6&W5< z34D$z+J2~Ow@F!$rUN|*Ilbo}_Ljs@O3GBS4i&qmMC@X}J=SqS}B z!kFF}9-L>SDwm`Q>VIpwSvjMOXfsQTZ#S zqPQ5MpdRWJ2bL%hs6rz#RS|4*67=nS9&ELjNQ=9vZ6vp*#eA)>m;zKma(!WHBi0t9 zGk!6KxEfd~BQ3K<hupP?<#2I$>T+~b;8GyCY9q!(Dm)k}ieGn_OtY}^z-2T~E+N`R@a0Gz z6>WKj*6kA%~nh+AymICX|vLy1fM1XX*LZ{NE7`h|1zrl;QE5K2M>PNyR5 z*9qVi=P@>oB}AeR+gRslwL!qRTQAcOP9G+jT9^eG>87<{?D`d)FGJs-1{(O?kBt}v zN3^S>>!>3ekYkI5p z0SOg`i?J9)+;-t=3~ctf_&AAsH!=Z}rEZalCOsK(eFJ8d|kr@8y$ypiSy)D|p7 z@lPpK@?fBkfRRCA{^iZi^oeD)@=o8>2H#Uwr}+tED2BdVG16Er>~cu+vYA5QXeoVM z=!}%vX`};={<+Dy{<-rQ8|_nWj(yy0Trtjeju;1Lrw4o76Y*ACip{zeJiS+(f2DhJ z4_b2AAG%lnp}X+^8{LKNTnw#E{+01^xPLMJBTEOJ%lfqhwOyp6!~3G1u8FQOC9p{< zU!_c4z$SDH26l3~%E>HzaP`mDmM;_Ahv~NO#a}{()`pcyeCK!UI_Bf%T7aQuc=E+` z*GW$7&YvCbPWtP|Os*feJv0q$?oJzv%kjcspsMmZ{zt z_5Wh+E!*;1w`|?uPH>mt?(XjH?(XgoB=F+y9^56k6Fj&(FYfLRIb_bNRkh}+^I`8g z|6yFMwfkwk_u)LVy0)3IX{x&*YG9O(sV-JTigJL#CUUth>Qz=#nEc`z&cs=IwH9#O zIC%#cXDyC+0MWxSmQgU)F@-wa z(A4-CsYgTpzNH9!*Gxt(?9eWfxnV`&vuJ7st<(SrphU-GR>>f8(y!@U+|OFhEke{! zI?7zT#iodRe!9?hLq)skeBW2gy^O7zl7D`2fAm?8N>JJHWi?I#AzETiyR7?qHz#+O z+sbJV@eXMiycRcO6@d(j{i~*0g?3{X-BfSg6^+8VOjuu`-Iw(EHA&~hq>`0#ERN=j z#pyxjo~8g4^<+`O@Y3nBXJA%Rd6{^$oj;*<3dAoa-7Lag`?II@Dg@FqNV1nI;7AV&P2%uw)KZ%TDy=f{RrtoY}a+%ua%OGZB#8&b7UoxMV(K&LKHzx~ERagLBKmmZv3i}e) zg_RMPqzr(S+4D401Qch_=n%7X(&Oplu+c8-sfKA5ogR|$m*E2Fjc=uo!nR2v&B(`< zR_+j-;~S6Z)jbR!nY|PNzeFl6ZL|lc#)`)7LeTXc0m@Kf(RmP6%x3vBX}jY+AnlI& zp!$wayXzAHy0MI`q$)l!ihD3;t1T4#ts)0Eu!C6gug$g%jP$69tbnfFNS=7iDvHYVz#&uhj9*vC0ga+>?fSk0!Xu_f0`c%c{rcC?B;i3~<4 zjmF?CT2ii?4N$ zty1WPg3dNB$LuU0#+*KMo|0}-2cc#N+4U6g;{KP;7cdBY=)C9t#9Kj`0G|u0UJR}G zEhe}Hu#NBXZ=K)z(D{K4@2CprOt}u+-1UAR6c<#E`8o1^jw_=^bs3ZiK4iA&_7^AL zU7!XKfx-rb7H`w;pmab(P&{RyUdtt|@)zUQ&Wa^qz{p(-i~#9{=t{4@%L3=|I}_prrK-k|<-k;zf? zyU1p#V2NPrPo}`&6ZbXzcHmXxKY#myQ9rjH{h{=WA4)I%zft+|C#AS{$%=+iLV*jij8VO>Y|B~ zh5=Ov>H1OhK2^QGrsolNDT>a5%(f9XX8MW_1)YUPHGR69xhJvtU2li-x;YJX8b82K zEIie_-hfNDiSvHA55;Gc?bD+e%{X@qq8KfE7yg~@d(46zMZ|8WfiKr>n#SFS1+j;5 z^l7}w*$g;cKH)9jC>>*$j|AAO4~Er^i83R|^T}Q5k?HoWOoX zPp?AgUifI#iY6Nzb$exx`Gn(g5%|Ds?YNbpeiWlWs^5>K;FTZ^2CvX`ZfX+P6!5Ux zh6+dN)<}l~_sePJsK+P8MhDF9`d+Wq?nIs_&H{54wGK}abXbu8PzpsV@64eWgp8L! z?KtH9dZ%3|0XKLcF_t=1k~{!7)oV}5ReRMJp*%|+Emao1hnh;bfr=C=wJOR?(we@D zL8L%tgT>Kw!8X-H+*3#@fyPBPtVG5>-;$ZA%Q8=Ob#~ELl@9^|kU@oVa+y1s{S6k@ zYkE!YeoQ9*5F8-nL&?XHlWtV1GXtE~vti6mC&BmErzkoIHVMOvWcyJ4qRkq+qZFRY zSFm|%D|fSOqDp_=BB}43t9lFgBZ2#-UP-?c@@*8_5zzg||LPHTo9SuIheGbiet&>?*77MM02krb=^iibs{4K0wcKQCwMB#De@r0r|c?^6V zljszx1gg9^vakUjog^HU#92{s2P>t>=mf+PFGuHuUMH{hit$V-{+0Cr+sJdFQ3hn} ztjxY8dbI#D#UrxiUpR zL*X0^M~;tXi%Ac?w8)p^^}zNeM*=HjCjDTf>?&0LV(DALI?)RRwh27NZyj@M@YFCP zY7qnP3of77E={&E6rev|s#4UJ9TJDm?*XbH7~*Q}vMGv?E$H!Cen$-_I)GG%NaF0O zSr~C~a2;jRqS)=@`!q}MmERU;TlY?-V^(AVZh`RgPaCiJye+M%U^_Z7N0kn+L6!kd zdBNx{3=FJ{S%yuF=u*|p%V(?9uJ4J~du?rBx^zRy;bpqD=@<@UYQiz;WOUO;TIC(W z@5{=`_Du&jA(tg+N8LLw=p;yh{=r@3AkPJPWK=}rDfiyq>C;%#<+HO)!9*}N?(XcA zKSB@BJOkdVp?>|A)TZq9`1ycOI>~s3Kc3+FA`=|cH?ABO{EH+Dd)VETuU`xKL-i}7 zeyWJ(5*Ld)As+aJx$uixAP7&`8!t)3*9#Ca7_wgtzI~{EZvMpRvm9Y@y5M0|lqEYB zg31hL=SR8++iz!`q1P1#U+vz?Ogy|ChpAx^jyVTFx%b@&JpPsLLD_v> z4&J9(i@I7v=p|VB?%{;%^*7F6U{I=jbK#)9k%mwm_m&@WF5Bo0$amzrOi=q zG9POfD#$<%)I>#gvcG~7J}E*3p`;WFrsEf9CIag5r4O!=i1>n1IvRhhIvk%6mZA>e zOAN<$3k^p+3O60_L;w$;5b|mcpE>IIbfD#sEI+?>-LnQSiF7l7^RX9Nw8+H~UmN5z zAqO84dJ4|C0&9|4-4Eb+EVipM?K^2>Pl;hGtX^u;_55XSF_ofy?P`=d|%ynf!M>7PF!T zvc8Pu#>%KNxN`6_9KQ?L#rZtkzq5%MBP?h8O!5(S`%JVRti8PC3jBuMgjdJr@AmU~ zUMmR~tEwFwH{KS`uT46XL#tHQ;$W3%9_va_+wE^|?P2SYYRM1p>V;*f440N7rKhz5 zY<7lUI~z#RJRG5zI4bV6f|S|ht~RE|fwz?6w$%$IxY@%6Q4Alw>;7K0-DZJ^BH}ht zI|c2o#LY6wiei@eub?mfub`juC+L&8Wd9ZPB|ZKG{d}SOedcBgIv20GDL5M=a4ctr zSPb#7ysysV&hWr7Stkh^>lpU_O`56~st{R3BgiK4N(I-}5fn8%L(e)t3jDjq$2Er+ z`MHt>)e_0Ml77qLkaqQQ6@0mo3B4k-7x{@)vsCS!h;0==yROQOB2ZBXk1m*}xhYZ6 zs^r(JA&)oFMX7KZk5Y*+@&LZswHzv{+VFyM?Y;6lyFiST=vaX+3ZfX4=0HnvSCVPD zgdF<{E9;xOs$U+lLM7gymT3Vp%+90ErQU|;c(3|zIs~2dsUfA94Aj!UKPS1_rmwj| z<`^h;1O}7T5!>ZL7>dFyO+u$(CZcpritri?#7D?a{hUZ+2)K0>A57#iMT30_|qDL`hJv$Is8|fBD z%wjz!Fr43Q->3e0*zpcC!h51zhn%;*>81=}k_xFz9=%;-#48}+`RieaDa4~R)4Saj zyT8G7pX{csA%}CxTb;U#0bSy=dRAh@4U?;hsxo1V)g;RtA4>uk!cK7qCb{wxmb`G} zHU>tSUE<3ecBAe0mAgh3#Lj9J?WeXTN`pp{#J1rYb?L~pd(IynR?a?iQ^A|AE)DV; zvdbLxZBtVG#u+I)T?3$Zs$fRmX1=!tj&p}1f|#J#bO?xeu=JEVg`aogqe_#iV`*R|YTl;y(O_LG_8iSv=w3OA`mp%fyLq5I`X= zVQ8H|iNqOKDJ#(`2Zw3$pG9rX1ELNA$3EpXL^WIM$C?~2u{@lmTlio04?PH#d}8Nw zI_O!LE6ZsKN0cDvdxi^gv0p#;K~&cA;>P^Q`||x8-oZ4^nRFj{f4TP~@56D-&0%0P zT^g}zjG|0Lhh+=`-$?ndzdM7Ik=db`*{8|VGiKRq@Mr*_kC0*Qn%N(&<-Fd&*qB>T zDZTGGerZ|d$~^eWG-dL26Fs8i|2A*4!0nL&JyY(X)HDS4gZw3jhy-t;t z#S(f3Qz%7w({y?>w{Pr{KDujXI|-Gq|I#I#@C)?SOZ28RFWx%dj?<%V<(xz{6Jxi`kDNj2N@&aT0G@Mk~cZr zFpW%ksSs4DxL5U%a`A;o==pIm$s>JPNmvl!NGQMBnE#TO4}WC*}| zvbs5HS+AOr*BG6F;pGfpF85#-6>}{{$hu^61}Q_1F%}LFhiljvN{ql^LFoXHr+GnTd4kQ0B9Z{QvQ=MIOyUxPCZ z9ADsfNxtY57gjii+`g%}5qca6$XubAwKZ*VDB6F|q&>?V3xTBHo?%ZD`t032Z8T*u z<-lm@_{8z=pX0-FI)BJV63P8YBEtW7pW{E$$G=rc-(?;Vi>A=*Q20@1lvjl-1gj82 z)bor%8ubz&FxcCjE&%aPr?+LFyB3b3x;S^f?SM_pVaxnmm1LEzJ~%?^bJ2RR<1xwi z;NkB7`{uv^B%53|{RJnNnIMH@<2G@Z+LQ*^eO)r`C{ zU6P&h5h&OSZKG@rXUnr?|O3x(MCx@%%gt?ka2p|)A1;43_BSHb5AF}9nIuk z>-`LMYTOk~p~6)jvve9%fWGEuA>&`2+``a8fWb1iQ6^UwmMSi5CAT?bNCT@r1v3+4 zXr@{>Wxw)BwUokbq`t)L)uXT)-~m5RGYNlDHFpRJKbJHM-CCn=HjZV2$HlKRo@SRW zTHK4U;W~w@0Y9y78Yxl;_ilveq)k)JGR$Y0*o#Lgdn62vb7jPM*-wpAx^zi+OQwnW zs*Tbny4DuUOGQHu;G(EjnNsGY&2ekgyXl_jJJ08xkSm&`SgE^l#1y1lm7?Z2HeADM zD%mK&H~uAZ*fEJGZLPgXnbkQ)_q}_#`fyCaHWK7GO8imm@G-}K)k*QXzb<|_*cJS%PMS)YkJuvr z`<-#Nyt8_lq^$M7>Lh~ys*{oUi=I)6++E%@_q83Wnf7eM?f7MB;QX1eC z|4}D7#6^~>I{at@>jUogED^=j7TcHpsgsyBQWqM6SQ5Cg%>UF$M=0Tg5GIJQi{D&8 z&pSp*9`phI-cfm6>E85j)B=`^#oDgUvHb_8x`v$Z8_Y@~Et2rc*pLYf+ubdDo?;vk zuH8zy!u&xVf@?M}m?cQXVQJQ_vbm6ezB!7QpL#v>(8X8uZleCN^oUF(NnUv!#S@(y^36mr7t|@)WwJ2xUx@730dSu zow_seqFpbDp1B_nCFG=(WgAGx?`{+F+4GbN=8?fac+>mqI>QHVUib#|x^Qw|88BFVy$@wn&hxBB2s*#h_tmEc_B{9q?Y3%)QG0#Ofi zVR;9WIg$8T0U{d1rmOw8KW$R}#P~DaUu}|I5dPZrn3Jpb zewq&Umo$unDgeheW1IC>m$v6U)THk&*`aFy-Q-%QKdW1bg?-T|vRLeE(9aH?<`?}S zQJTpEyjS}E>7~_JM=jg>=m+EaRZjy$`<=FgmBDRx{_+KDv7fCJ`Nd8i0)SY)Qam9T z8Qt$=u`}aR1xNBlK5oHr1bIl^t!fn<^LL-;!u#;WrX#!MKX_BO?c#$sk6#c1YmT0q zXZ(1eWsxlj`8HZ`gii|&WQ1brjpth1tA6V_^T~b9*+H7++2t* zu1VxoVVbWaDoYk7}41gSDW!yW#wqu=*=OheT=`I|KbR9|A zOK2P93aOBIx&ag>O?fWwAeYe&PT2N)te$Y5Mp& z7=5&pTZqBdIZ`EkY$|OajZos+uFfGl&Sp!aQBRX~i^$PR&WWwT8b7PiqK_V(A(^c^ zBij?Ia<``|S8)J8>~vc?2v;z($n}oJKo#yd&4}QUO>47;EUJ*w7YZ916J5x!_7jVk z#l3I(iK}lHo&q|_ApcXF-JZ?bH134Aem!-Jo$7VI@_4(uh{3SIeq-A(g=_kGU|r{6 z&0HTeBU%n2fp{1ZzgnZ-Z0)t#xi$Lu{2-QZBb&e(cA`jHpqkX(4#OwZIy!YGP-S*Lp7_ds^tASp+x83EJY3{<~%ZA|RYVf)tVNF}1s=x~oK>}Xy<%}l(l=2yh2T~doedMSt$kD~+)2UwY)u!RpbVs7 zlN+28u!mt1c#3%OHxQ{=WFeDk%j0H9zUCKtpZgOYCx~HU4HoM^hFPk_c6`b(R1%S0 zUT~gfb>)Usq!te$O3TR|8=L7E1EN&D9gP)oOsHrKpV!mbw-zWclm-OYFD_yWzZO8= zG3D8Lgz&(_x;;t9Yp04X<~ZT^X`4$XWpYhqbvjnL5kaq3hGqm2GeuzIaM1SoecN*M z?z(MBRbRCB>T_;2u`++BHJHIE)@h*I;zj^AX4mbjwz8z8M{lv=MSY2Kh1cVp0(_}s zy4_TxLYU1_N}rS)nY!`Ru#3;t!lbW3m(!^M#9M^19ak&unDwzDAoo!|lVAx6na|+n zl}h{)v!4x=W&!QPJWA#CNdH~lRJP zKWB(jN}GJe;t}dZEe~HeDv+xKzek5tp_8B2QnfTpsw^vRgep8|4ckwNTR$OYARB)p zIcApWbSN!}-Y4}(|I`&YX)|-w!y%6HoHG2kw8%~BLjt#w-$fFVgqG{W_t>fLnr2mB zwRDpFBlJ`N0}Slek!w~~#$==0^3!u4)L8RUf}3J{ttEnGnp;b|diU-C>csuP==aM{ z@)xdzvhX@bM!~IJ9*^J-RKrRn66j1u_8)B2Gle`iw7f)g7D&J-zvY{|9*?t-e0GNl z;;<+3{TxbB2lmawN%fi8SJDLOlqlaCH)iV&<5U!`sfGvXOIpH5INp(ir8*!TB9~uZ z-&fr930@)@VU=&C&|!7TNRRq*h0DyPe3$3Hdb-Mf@YOOM%3b_o9b0a(#Oyq;ZI@o` ztB~VF#beV!pDAI3uYAvi(y2mLTWCIfeYo;=FZoD<3+hE_y4;xUwsifJO+rk*G}o>e zVZUu;xuEJyLK3aZPBRjGlm*nBAB{=B>u0#(6WrBIbkG-6C9O66j*VV}B2%)iIQ-|B zT3nG27Gw*0k3OBk2vQN{7QNue0bp6hzg(y2LgZY!6CtU;r{JQB#}RB2tx(mR=&z62 z)M!}#e$`KW%A4+x_70)7gS$*cq9T$eVWUGhBXc!e1WgNwcm!>OK>5Rb!Xtd^OEgmy zQw(uvSV&8BwGxIkKlvxQ52If?neP6vo@b=kCE^D8>C+{`|H*;%e-mUCTPssDVIvb8 z464HQR->qLde3Iz~SRo6UeChDf?vrKuGejiOSh} z(T@JZZ=d;f%4RStd~1DwQdcnt&rs8lJEZvkQuX5EPPJRSJ z?-m2Y(@05Gn%K)%o&(}SD&@dO#=CVf4o@QHkav~AF5Be9Q%Wkh9$?inTBx-h^Tm4x z9&dzzdnMJ^0Kz0pei^;w=Oh-n1*g>aF+48HCH3O|Y~cZ|i9_}2AJN9*XqYV3dQ`Ng z^w-MEyCe|U=kHXrl-Ek!6Kp^#YG@|Y+#E5X;5@2s%l2X;GxC-SOg9ZeQ|P3WAXhh8 zL|8DL!m4J3)?<>grOBnA*|>hwf;vB6fUQcI4bR7_m>J=&^a>u~f`6eEY zxrRmcfcxEs}tsveCC_lmeDt4`n#rrMm%4^v6IDIA!3g09%*Vk?jC7}DK&Gi>pj-*=ryA&3rv!G=V@ zbWHd+g)n{*FD3zDq@Z$B>7t*JQ7B*_-}Vd2O7_#SFWUOQkGXjCN1A?_Zw%2qh7nMQ zqewf>$m6@()Td-4!4DZ`)L_FdDNmfY(phtUA=EV`)P+ZCGE7ayCVs7gMdyoOItTSQ zge)bajZ(@^JX}$!76D=Erzj^<$mafCyupWsL*UI&{slHp2|OMZVAj${oq_HC;^yI2 zmc&Gx%9CPm?wnGHE-pk_x1imD=-T7XpelPK2Aq~zOU}cS-K#m4@gAJ3dK!bP*YN7W z_cbiRSv6cN!m7gx%O?!oQ=8nq#(m-)IJk{C(#ttfg{?l5J)9u+;C&^+ix%#wXJRcV zLNwyV6ru4hWW^G}^)6!V|Jj@Xr-1)i2Nw1)v0)CcN z6u#gv${5__8i}U>ZQqJH-l4r)^X}*6NuqeC6qEhYT5QL9VoDErV1L|Cdant*zDBy_ z{olxaAX%wQyVx0JO@zZzfV0m;%5@|NWuAcv&%+%ju=@GdGuYlLxVx_;ItA!`w95DW zj3JRqvfE|={8%RCOIY+Wefn9AUV%rOi(Aikt1;0lYI+rxGBEY@<-E#vTvW${I+Xr+1luiE!3{%1C9itwYJ68DWB!wQXL z9I~xR%BbE?`TR89!K`OpH6f`Nen5pnaabNrEY!_cJ z&@A)8)emhb!dh&7NFhvT3%KZY2*#st2_^MH1|O`FiBA~E!*6eGfLCKUG2kMn>@jW| zy;uEoS%)1VTNjd_7vzwj7^RcRDONku` z-5!wVj~y`%d}HeQB@|>hMHSiJ_+kz!e679Fcle9e2=6&Hn6B)i>k)fi!sa(5`0!it z@%gP#$8Q;8YfTduW^%koA&*gMy1Y5=ACEcp)C2Y<+rGt|<7a;Hnb!ISWqrBj;Opkh zav-cqh*Bl=UbIFQ{o7hnXV-zki|d?IU1UHq{zB`CCU z88grkbyB?c+1m}dKa|y9^o82$n4Tm}gf-L~r)=BO@|RMhZ96>q_q-!!cbI;NCOB5O z2IvOZQzXlq!EDeoP<&7rqH%Y)Q{)e8ql!aYqNp|44EV_c3dQ%5#NqB`^8AmL46L^O zi}(*?Q~5A9$^V71i8$E1TG_ifxVikNwVkNoE~5X|>vTts)G{R@fXw_QPkdD?I4FuS zScQ;>0n*OKj=*WvQAuBG-dJ&}H$62M^CHQA>r%pUmMZ^}H~YCEabhmm%7VN4rs}s6 z-|SFpJIZD-v)l=G%fZ^sbGFX~m&IMP|N9Zcr@W!G`Q~f+!7b%DiQR%BsLLQ z4@Sy{IX^+wfoMfODL7CUVHk$^xL#q-{yH|Q?xa;!IQCXtyg<}MbcaeU`Y_y2o=D#B z{sbH%%VwIVa4&jNARHw(fgRy}m*_p|7W$E?Gtm>0A!)l&*TW!ygT++dfk3QdjP=#UPO zlhQ_qB;#qXV@}l}9_N0r5YDw^=a4~`RICPX=YouOzkza+Z0Vhn?k#!?kXjb3?!)&# zy)1V0bh)}T&muB!YqSG^`*+LBNq?{I?%^8&XnUGuJSS*w=-u*-YFS9q1rHfyd7X5M zH?uVOZ%veLQ~8J=F^%vz4LH;OrtTX!m2O?8{oGXUHG0*0faU%bP=oEZTJov);kfw1 z8xGJs+&R3K0N@o*RAemPLIda^4T&LGMM<6~0IK7BL_|2~CC{1rL>&tZup@|ozA75K zn9U)R6|+q?_S7xH%?343>d6e6O?EH{8q>M`XkFwtCsHWqY|QZ-UR@}}jIY!$%p(lE zBVZ<@zg7YDIo*xV0h#Zn=X*)_x|+TWBxwMRtKtqA?`M_ewY;Rh6{3b)Oy4@8P}wp{ zmX7z0B11~g$dL$bQ_>f+FopvC8XfnL9F0Cudvc~&9eBOpJ(4}mrP|Tj(frQve#^@{ zy>#31D9ui(Iewmo-JM6Y8!2s>%FVRjD)9(+b69;tpFKUlb74{rdXyrBLRG^4crc4= z-T78I@}-zHi+?>%rjHGQYb|a~oKfuXdi8Ubx!vL=`cy#pG(-}csghhTpwDspr}P97 z1e+BrCXSa>$LHhMiBseO=|sV0=2PzOM)Js=kVu42RPA)W8r!}<9rsHI>3FVLdzp`H z-uQ5&07Dx|sFBnM(ESC@QgmNmt+zf^vjh)%%zA0 zIdT1)fB&Pb1@ZbOMRD(A0s}jdwxx=-@#!{h?zssmGD*O~!hvPM%i+Kgq%@S$GZHa< z-*0^afT)GzJ@j3_ya=CTVcU}GL!Q3@{<22u*G0u{XI}Sx&r_EnlUxCU%zZ-Ae>{^ON?vd7Gmus=PnSdS?9|M5M07 zhDkxrdM-Moq+_h%3L56}I}^?)j4~;cK#;egu4up+k><3Kskb#4OCf9^?A)a2 zYwMPyeK;r^8yBDmpVyp<{f=Y_x&6MgoXLt6XCG<$orwI-<(`do;gFe&P_-g-zG!{O z+PKL2Fg%KvlU#v!cteJBMqlrfg~Pzia49;0(Q)BzLt=sCL)qObW2wJJIjZy3HX9sV zzS#sNI*X&~Jz?kMqVMHP;$_Xi_wb+=9aZP6AJ32tb@|~;-b(7c{AAWwM%gN~Kb+vT z+Hau?%VF*GVfMNApx{^jiA8LpzC3ZW)7FCx36qpV4COT{i`kmZuzF)PPP>$%FB#cu zyYge?DgBm9pBCGfk)aRBWPS+ob*{}`fAKU3n&fAkmtO{>F#KJs)}J_4Am=48QY>t+V681@+>LFa<4zKXr<7`mv=l z0Z=W2aDC#jZ8^S4Q}V9yvF(jX?5YeTA*dRSnCkVg0tqD4X3pEn>H11fV3^5jXBf9I zndU+rSCU*A#vO)5tyeZVPImbcvhhb<1$!fShStMiV(f)fo%ssz&pcnqt~yohg2oah znpaRtfO3JjxDItcCIPU5L`+XisX`nT#4KA|B5m)IZ_HcG?^ceIzJ|V!G3jOUv%Ers z2LdfPoaKFk;h~q@lg+dqmWWVE!7vPfB0}?3dop@#w zR+MrnGVl00d=ImNI(ZI}fI6cT2Vak#XjIOlm76jwS8}i{w!bXKV05Eyl|$5DlU#Nh z-tTPQBF7;pXJj5moh>?q<6;XNM#QcR<8^viHmd*Iu+-&EU%B zXYoot$nLUmr|!9H%#9`fwhrxGtFeA3%HEtXS>P*8gL{6)<<51Xc^_sz>AX5{(en=7 z>q!y;$-3|*jHAF(xFqR0-%>m^=^6V`J7KGF$H-FKyg}64R-dzj(^T_mnGumibd*ej z;Tjpp=!!gm+#tpnM&uPHhYWjzmHul8s8s4{_;{vPpkIZz=z58nbD;)j)F_!BW>U{< zm9jOMQGVkOV@x4G{KXj_L3M+nkjzftv(!k_T+Jpt*?q;+Vy|Z=wY15hbamVVgDpPQ ziFhD0Wdu`Jq#@b`qpfea64r%so^=t5AMNY6?q^ko1HvIua7@Q|Zsmify&NeEABFM> zsd+O1XJd&P7poPgRA-4xCjB%-IJQ1M3mMbwjH9KSh#ro9bUuDbk!j0YJTv)Q#uEwv zT@1w>oJ_Z%_+idsg1Mv*sqXXVW{V;vE>I|!xheHFdFz-6^Ba@8t#=!TilF?oA*j0M z4?IZj>aZWni|s(Ja+S|aBGfo>X&Y8ZjZ9p-chA?F-Mx>?gXy8_k}EJ%X^OgOo01Z+ z%t+b!iUK>Q0vURgITV{_>qq)zx>LYKzDh9eb}J#79WoeenM``}W&CRHu_#8<8|LK| z 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 Xn_!>Ie*ggC^D6p`#$lgL0KoqNTbN7V literal 0 HcmV?d00001 diff --git a/app-glasses-vm/src/main/AndroidManifest.xml b/app-glasses-vm/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a9888e5 --- /dev/null +++ b/app-glasses-vm/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/GlassesVmApp.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/GlassesVmApp.kt new file mode 100644 index 0000000..1a6f7a1 --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/GlassesVmApp.kt @@ -0,0 +1,46 @@ +package pw.binom.rayneovm.glasses + +import android.app.Application +import android.util.Log +import pw.binom.rayneovm.glasses.protocol.ControllerProvider +import pw.binom.rayneovm.glasses.protocol.VmGlassesController + +/** + * Application-класс GlassesApp. + * + * Запускает [VmService] при создании (foreground-сервис держит WebSocket сервер + mDNS, + * и продолжает работать после сворачивания Activity). + */ +class GlassesVmApp : Application(), ControllerProvider { + + private val controller: VmGlassesController by lazy { VmGlassesController() } + + override fun onCreate() { + super.onCreate() + Log.i(TAG, "GlassesVmApp onCreate, uid=${android.os.Process.myUid()}") + // Инициализация RayNeo Mercury SDK. + com.ffalcon.mercury.android.sdk.MercurySDK.init(this) + Log.i(TAG, "MercurySDK initialized") + controller.setAppContext(this) + // Регистрируем debug-receiver для тестирования через adb. + val filter = android.content.IntentFilter(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver.ACTION) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + registerReceiver(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver(), filter, android.content.Context.RECEIVER_EXPORTED) + } else { + @Suppress("UnspecifiedRegisterReceiverFlag") + registerReceiver(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver(), filter) + } + VmService.start(this) + } + + override fun onTerminate() { + super.onTerminate() + VmService.stop(this) + } + + override fun provideController(): VmGlassesController = controller + + companion object { + private const val TAG = "GlassesVmApp" + } +} diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/MainActivity.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/MainActivity.kt new file mode 100644 index 0000000..a039235 --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/MainActivity.kt @@ -0,0 +1,43 @@ +package pw.binom.rayneovm.glasses + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import pw.binom.rayneovm.glasses.protocol.VmGlassesController +import pw.binom.rayneovm.glasses.ui.GlassesScreen + +/** + * Single-Activity приложения: Compose host для GlassesScreen. + * + * Никакой навигации — здесь один экран (зеркало гостевого приложения + state overlay). + */ +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + val controller = VmGlassesController.fromContext(this) + setContent { + CompositionLocalProvider(VmGlassesController.Local provides controller) { + MaterialTheme(colorScheme = darkColorScheme()) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + ) { + GlassesScreen(controller = controller) + } + } + } + } + } +} diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt new file mode 100644 index 0000000..d4aba44 --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt @@ -0,0 +1,245 @@ +package pw.binom.rayneovm.glasses + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.net.nsd.NsdManager +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import pw.binom.mercury.transport.ServerTransport +import pw.binom.mercury.transport.Transports +import pw.binom.rayneovm.glasses.protocol.VmGlassesController +import pw.binom.rayneovm.glasses.vm.GlassesScreenState +import pw.binom.rayneovm.shared.KeyEvent as VmKeyEvent +import pw.binom.rayneovm.shared.LaunchAppCommand +import pw.binom.rayneovm.shared.Ping +import pw.binom.rayneovm.shared.Pong +import pw.binom.rayneovm.shared.SetAudioMode +import pw.binom.rayneovm.shared.Shutdown +import pw.binom.rayneovm.shared.TouchEvent +import pw.binom.rayneovm.shared.VmMessage +import pw.binom.rayneovm.shared.VmProtocol + +/** + * Foreground service на стороне очков. + * + * Запускает Ktor WebSocket-сервер (порт [WS_PORT]) и публикует себя через mDNS. + * Принимает подключения от телефона и маршрутизирует сообщения в [VmGlassesController]. + * + * Жизненный цикл: + * - Запускается из [GlassesVmApp.onCreate] (стартует сразу при загрузке очков). + * - Продолжает работать после сворачивания Activity. + * - Останавливается из [GlassesVmApp.onTerminate] (или системой при необходимости). + */ +class VmService : LifecycleService() { + + private var server: ServerTransport? = null + private var nsdManager: NsdManager? = null + private val registeredServiceName: String = "RayNeo VM" + private var nsdRegistered: Boolean = false + private lateinit var controller: VmGlassesController + + override fun onCreate() { + super.onCreate() + instance = this + controller = VmGlassesController.fromContext(this) + Log.i(TAG, "VmService onCreate") + startForeground() + startServer() + registerMdns() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + return START_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return null + } + + private fun startForeground() { + val channelId = createNotificationChannel() + val notification = buildNotification(channelId) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) + } else { + startForeground(NOTIFICATION_ID, notification) + } + } + + private fun startServer() { + server = Transports.wifiServer(port = WS_PORT, path = WS_PATH) + lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) { + try { + server!!.accept( + onMessage = { connId, json -> + handleIncoming(connId, json) + }, + onConnected = { connId -> + Log.i(TAG, "Phone connected: $connId") + }, + onDisconnected = { connId -> + Log.i(TAG, "Phone disconnected: $connId") + controller.setState(GlassesScreenState.NoConnection) + }, + ) + } catch (t: Throwable) { + Log.e(TAG, "Server crashed", t) + } + } + } + + private suspend fun handleIncoming(connId: String, json: String) { + val msg = try { + VmProtocol.decodeAny(json) + } catch (t: Throwable) { + Log.w(TAG, "Bad message: ${t.message}") + return + } + when (msg) { + is LaunchAppCommand -> { + Log.i(TAG, "LaunchAppCommand: ${msg.packageName}") + // Реальный запуск — через Shizuku service (см. VmShizukuService). + // Пока — переключаем состояние для UI. + controller.setState(GlassesScreenState.Running(msg.packageName)) + } + is TouchEvent -> Log.d(TAG, "Touch: ${msg.actions.size}") + is VmKeyEvent -> Log.d(TAG, "Key: ${msg.keyCode}") + is SetAudioMode -> Log.d(TAG, "SetAudioMode: ${msg.mode}") + is Shutdown -> { + Log.i(TAG, "Shutdown") + controller.setState(GlassesScreenState.NoAppSelected) + } + is Ping -> server?.send(connId, VmProtocol.encode(Pong)) + else -> Log.d(TAG, "Unhandled: $msg") + } + } + + private fun createNotificationChannel(): String { + val channelId = "vm_service_channel" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val mgr = getSystemService(NotificationManager::class.java) + val ch = NotificationChannel(channelId, "RayNeo VM", NotificationManager.IMPORTANCE_LOW) + mgr.createNotificationChannel(ch) + } + return channelId + } + + private fun buildNotification(channelId: String): Notification { + val openApp = PendingIntent.getActivity( + this, 0, Intent(this, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + return NotificationCompat.Builder(this, channelId) + .setSmallIcon(android.R.drawable.ic_media_play) + .setContentTitle("RayNeo VM") + .setContentText("Сервер активен на порту $WS_PORT") + .setContentIntent(openApp) + .setOngoing(true) + .build() + } + + override fun onDestroy() { + super.onDestroy() + unregisterMdns() + server?.close() + server = null + instance = null + } + + /** + * Регистрирует WebSocket-сервер в Android NSD (mDNS/DNS-SD) как `_rayneo-vm._tcp`, + * чтобы PhoneApp мог найти очки по сети без знания IP. + * Без этой публикации телефон не находит очки вообще (правило #4742 врёт — + * публикуем с очков, не с телефона — см. SPEC.md:106/304). + */ + private fun registerMdns() { + val mgr = getSystemService(NSD_SERVICE) as? NsdManager ?: run { + Log.w(TAG, "NsdManager not available") + return + } + nsdManager = mgr + val info = NsdServiceInfo().apply { + serviceName = registeredServiceName + serviceType = "_rayneo-vm._tcp" + port = WS_PORT + // txtRecord: version/appId — для диагностики на стороне телефона + setAttribute("version", "0.1.0") + setAttribute("app", "pw.binom.rayneovm.glasses") + } + mgr.registerService(info, NsdManager.PROTOCOL_DNS_SD, registrationListener) + } + + private val registrationListener = object : NsdManager.RegistrationListener { + override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { + nsdRegistered = true + Log.i(TAG, "mDNS registered: ${serviceInfo.serviceName} (${serviceInfo.serviceType}:${serviceInfo.port})") + } + override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + nsdRegistered = false + Log.e(TAG, "mDNS registration failed: code=$errorCode name=${serviceInfo.serviceName}") + } + override fun onServiceUnregistered(arg: NsdServiceInfo) { + nsdRegistered = false + Log.i(TAG, "mDNS unregistered: ${arg.serviceName}") + } + override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.w(TAG, "mDNS unregistration failed: code=$errorCode") + } + } + + private fun unregisterMdns() { + val mgr = nsdManager ?: return + if (!nsdRegistered) { + nsdManager = null + return + } + // NsdManager требует тот же RegistrationListener, что и при регистрации. + // Хранить ссылку на лисенера проще, чем доставать её — выделим поле. + try { + mgr.unregisterService(registrationListener) + } catch (t: Throwable) { + Log.w(TAG, "unregisterService failed: ${t.message}") + } finally { + nsdManager = null + nsdRegistered = false + } + } + + companion object { + const val WS_PORT = 8080 + const val WS_PATH = "/vm" + private const val NOTIFICATION_ID = 100 + private const val TAG = "VmService" + + @Volatile + private var instance: VmService? = null + + fun start(context: Context) { + val intent = Intent(context, VmService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + fun stop(context: Context) { + context.stopService(Intent(context, VmService::class.java)) + } + + fun get(): VmService? = instance + } +} diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmShizukuService.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmShizukuService.kt new file mode 100644 index 0000000..bf9b69c --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmShizukuService.kt @@ -0,0 +1,14 @@ +package pw.binom.rayneovm.glasses + +import pw.binom.mercury.shizuku.ShizukuVdService + +/** + * Shizuku UserService для системных вызовов (VirtualDisplay, launchActivity, injectInputEvent). + * + * Запускается в отдельном процессе :shizuku_vd через [pw.binom.mercury.shizuku.ShizukuVdClient]. + * + * Методы Binder-API ([createDisplay], [launchActivity], и т.д.) сейчас — заглушки (STUB): + * реальная реализация через [rikka.shizuku.SystemServiceHelper] + reflection добавляется + * в следующей итерации. + */ +class VmShizukuService : ShizukuVdService() diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/audio/AudioCaptureService.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/audio/AudioCaptureService.kt new file mode 100644 index 0000000..9293e6a --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/audio/AudioCaptureService.kt @@ -0,0 +1,159 @@ +package pw.binom.rayneovm.glasses.audio + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioPlaybackCaptureConfiguration +import android.media.AudioRecord +import android.media.MediaRecorder +import android.media.projection.MediaProjection +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import pw.binom.mercury.transport.ByteTransport +import pw.binom.mercury.transport.Transports +import pw.binom.rayneovm.glasses.MainActivity +import pw.binom.rayneovm.glasses.VmService +import pw.binom.rayneovm.shared.AudioMode + +/** + * Foreground service: захват системного аудио и стрим на телефон. + * + * Два режима: + * - AudioPlaybackCapture (Android 12+): захват звука других приложений. + * Требует MediaProjection (получается через ActivityResult API). + * - MIC fallback (Android < 12 или без MediaProjection): обычный микрофон. + * + * Сейчас сервис стартует в MIC-режиме (для каркаса), реальный AudioPlaybackCapture + * требует Activity-инициацию через onActivityResult — TODO. + */ +class AudioCaptureService : android.app.Service() { + + private var recorder: AudioRecord? = null + private var streamer: AudioStreamer? = null + private var transport: ByteTransport? = null + + override fun onBind(intent: Intent): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + val modeExtra = intent?.getStringExtra(EXTRA_MODE) + if (modeExtra != AudioMode.ON_PHONE.name) { + Log.i(TAG, "Stop: mode=$modeExtra") + stopCapture() + stopSelf() + return START_NOT_STICKY + } + startForegroundCompat() + startCapture() + return START_STICKY + } + + private fun startForegroundCompat() { + val channelId = createChannel() + val notification = buildNotification(channelId) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION) + } else { + startForeground(NOTIFICATION_ID, notification) + } + } + + private fun startCapture() { + val sampleRate = SAMPLE_RATE + val channelMask = AudioFormat.CHANNEL_IN_STEREO + val encoding = AudioFormat.ENCODING_PCM_16BIT + val minBuffer = AudioRecord.getMinBufferSize(sampleRate, channelMask, encoding) + if (minBuffer <= 0) { + Log.w(TAG, "getMinBufferSize returned $minBuffer") + return + } + val bufferSize = minBuffer * 4 + + // MIC fallback. Реальный AudioPlaybackCaptureConfiguration требует MediaProjection, + // который получается через Activity.onActivityResult — TODO. + @Suppress("MissingPermission") + recorder = AudioRecord( + MediaRecorder.AudioSource.MIC, + sampleRate, channelMask, encoding, bufferSize + ) + + val wsUrl = "ws://127.0.0.1:${VmService.WS_PORT}/vm-audio" + transport = Transports.wifiBytes(wsUrl, autoReconnect = true) + streamer = AudioStreamer(recorder!!, transport!!) + + recorder!!.startRecording() + kotlinx.coroutines.GlobalScope.launch(Dispatchers.IO) { streamer!!.run() } + Log.i(TAG, "AudioCapture (MIC fallback) started: $sampleRate Hz, buffer=$bufferSize bytes") + } + + private fun stopCapture() { + recorder?.stop() + recorder?.release() + recorder = null + streamer?.stop() + streamer = null + transport?.close() + transport = null + Log.i(TAG, "AudioCapture stopped") + } + + override fun onDestroy() { + super.onDestroy() + stopCapture() + } + + private fun createChannel(): String { + val id = "vm_audio_capture" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val mgr = getSystemService(NotificationManager::class.java) + mgr.createNotificationChannel(NotificationChannel(id, "Audio Capture", NotificationManager.IMPORTANCE_LOW)) + } + return id + } + + private fun buildNotification(channelId: String): Notification { + val openApp = PendingIntent.getActivity( + this, 0, Intent(this, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + return NotificationCompat.Builder(this, channelId) + .setSmallIcon(android.R.drawable.ic_media_play) + .setContentTitle("RayNeo VM: Audio Capture") + .setContentText("Захват системного аудио") + .setContentIntent(openApp) + .setOngoing(true) + .build() + } + + companion object { + private const val TAG = "AudioCaptureService" + private const val NOTIFICATION_ID = 200 + private const val EXTRA_MODE = "mode" + const val SAMPLE_RATE = 44100 + + fun start(context: Context, mode: AudioMode) { + val intent = Intent(context, AudioCaptureService::class.java).apply { + putExtra(EXTRA_MODE, mode.name) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + fun stop(context: Context) { + context.stopService(Intent(context, AudioCaptureService::class.java)) + } + } +} diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/audio/AudioStreamer.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/audio/AudioStreamer.kt new file mode 100644 index 0000000..c3a4133 --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/audio/AudioStreamer.kt @@ -0,0 +1,62 @@ +package pw.binom.rayneovm.glasses.audio + +import android.media.AudioRecord +import android.util.Log +import pw.binom.mercury.transport.ByteTransport +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Читает PCM из [AudioRecord] и шлёт сырые байты через [ByteTransport]. + * + * Буфер — 4 KiB (примерно 23 мс @ 44.1 кГц 16-bit stereo). Задержка стрима — небольшая. + * + * TODO: добавить Opus-кодирование (30 KB/s вместо 176 KB/s) для BT-режима. + */ +class AudioStreamer( + private val recorder: AudioRecord, + private val transport: ByteTransport, +) { + + @Volatile + private var stopped = false + + suspend fun run() { + val buf = ByteArray(BUFFER_BYTES) + try { + transport.connect( + onMessage = { /* incoming: ignore */ }, + onConnected = { /* ok */ }, + onDisconnected = { Log.w(TAG, "transport disconnected"); stopped = true }, + ) + } catch (t: Throwable) { + Log.e(TAG, "transport connect failed", t) + return + } + withContext(Dispatchers.IO) { + while (!stopped) { + val read = recorder.read(buf, 0, buf.size) + if (read > 0) { + try { + transport.send(buf.copyOf(read)) + } catch (t: Throwable) { + Log.w(TAG, "send failed: ${t.message}") + break + } + } else if (read < 0) { + Log.w(TAG, "AudioRecord.read error: $read") + break + } + } + } + } + + fun stop() { + stopped = true + } + + companion object { + private const val TAG = "AudioStreamer" + private const val BUFFER_BYTES = 4096 + } +} diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt new file mode 100644 index 0000000..3f7fead --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt @@ -0,0 +1,130 @@ +package pw.binom.rayneovm.glasses.protocol + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.SurfaceTexture +import android.util.Log +import androidx.compose.runtime.staticCompositionLocalOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import pw.binom.mercury.binocular.FrameSource +import pw.binom.rayneovm.glasses.VmShizukuService +import pw.binom.rayneovm.glasses.shizuku.VirtualDisplayFrameSource +import pw.binom.rayneovm.glasses.vm.GlassesScreenState + +/** + * Контроллер состояния экрана очков и кадров. + */ +class VmGlassesController { + + private val testScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + private val _state = MutableStateFlow(GlassesScreenState.Starting) + val state: StateFlow = _state.asStateFlow() + + private val _frameSource = MutableStateFlow(null) + val frameSource: StateFlow = _frameSource.asStateFlow() + + private var activeSource: VirtualDisplayFrameSource? = null + @Volatile private var appContext: Context? = null + + suspend fun acquireFrameSource(packageName: String): FrameSource? { + val ctx = appContext ?: run { + Log.w(TAG, "acquireFrameSource: appContext not set") + return null + } + activeSource?.release() + activeSource = null + val surfaceTexture = try { + SurfaceTexture(true).also { + it.setDefaultBufferSize(VIRTUAL_WIDTH, VIRTUAL_HEIGHT) + } + } catch (t: Throwable) { + Log.e(TAG, "SurfaceTexture creation failed", t) + return null + } + val source = VirtualDisplayFrameSource.acquire( + context = ctx, + surfaceTexture = surfaceTexture, + serviceClass = VmShizukuService::class.java, + width = VIRTUAL_WIDTH, + height = VIRTUAL_HEIGHT, + density = VIRTUAL_DENSITY, + ) + activeSource = source + return source + } + + fun setState(newState: GlassesScreenState) { + Log.i(TAG, "setState: $newState") + _state.value = newState + } + + /** + * DEBUG: запустить гостя напрямую (без Compose-UI). + * Используется только для тестирования cross-process Surface на устройстве. + */ + fun startTestLaunch(packageName: String) { + Log.i(TAG, "startTestLaunch: $packageName") + setState(GlassesScreenState.Running(packageName)) + // Прямо сейчас (без ожидания Compose) запускаем создание FrameSource и activity. + testScope.launch { + try { + val src = acquireFrameSource(packageName) + Log.i(TAG, "acquireFrameSource result: $src for $packageName") + } catch (t: Throwable) { + Log.e(TAG, "startTestLaunch: acquire failed", t) + } + } + } + + fun setAppContext(context: Context) { + this.appContext = context.applicationContext + } + + companion object { + private const val TAG = "VmGlassesController" + private const val VIRTUAL_WIDTH = 640 + private const val VIRTUAL_HEIGHT = 480 + private const val VIRTUAL_DENSITY = 160 + + val Local = staticCompositionLocalOf { + error("VmGlassesController not provided") + } + + fun fromContext(context: Context): VmGlassesController = + (context.applicationContext as? ControllerProvider)?.provideController() + ?: error("Application must implement ControllerProvider") + } +} + +interface ControllerProvider { + fun provideController(): VmGlassesController +} + +/** + * DEBUG broadcast receiver для тестирования через adb: + * adb shell am broadcast -a pw.binom.rayneovm.glasses.TEST -e package free.zona + */ +class DebugTestReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != ACTION) return + val pkg = intent.getStringExtra("package") ?: return + Log.i(TAG, "DebugTestReceiver: launching $pkg") + val controller = (context.applicationContext as? ControllerProvider)?.provideController() + ?: return + controller.startTestLaunch(pkg) + } + + companion object { + private const val TAG = "DebugTestReceiver" + const val ACTION = "pw.binom.rayneovm.glasses.TEST" + } +} diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/shizuku/VirtualDisplayFrameSource.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/shizuku/VirtualDisplayFrameSource.kt new file mode 100644 index 0000000..abbf69d --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/shizuku/VirtualDisplayFrameSource.kt @@ -0,0 +1,90 @@ +package pw.binom.rayneovm.glasses.shizuku + +import android.graphics.SurfaceTexture +import android.os.IBinder +import android.util.Log +import android.view.Surface +import pw.binom.mercury.binocular.SurfaceTextureFrameSource +import pw.binom.mercury.shizuku.ShizukuVdBinder +import pw.binom.mercury.shizuku.ShizukuVdClient + +/** + * Связка между VirtualDisplay на стороне очков и [FrameSource] для бинокулярного рендерера. + * + * Жизненный цикл: + * 1. Создаём SurfaceTexture в этом процессе (он будет жить в GL-контексте рендерера). + * 2. Оборачиваем её в Surface. + * 3. Получаем Shizuku Binder и создаём VirtualDisplay через [ShizukuVdBinder.createDisplay], + * передавая клиентский Surface. Display пишет кадры прямо в этот Surface → рендерер + * читает кадры из той же SurfaceTexture. + * 4. Возвращаем [FrameSource] рендереру — он получает кадры через onFrameAvailable. + * 5. При release — [ShizukuVdBinder.releaseDisplay] и SurfaceTexture.release(). + */ +class VirtualDisplayFrameSource private constructor( + source: SurfaceTexture, + private val surface: Surface, + private val binder: IBinder, + private val displayId: Int, + width: Int, + height: Int, +) : SurfaceTextureFrameSource(source, width, height) { + + override fun release() { + runCatching { ShizukuVdBinder.releaseDisplay(binder, displayId) } + runCatching { surface.release() } + super.release() + Log.i(TAG, "VirtualDisplayFrameSource released: displayId=$displayId") + } + + companion object { + private const val TAG = "VirtualDisplayFrameSource" + + /** + * Создаёт VirtualDisplay и возвращает FrameSource для рендерера. + * + * @param context Android context + * @param surfaceTexture GL-текстура, привязанная к GLSurfaceView (рендерер создаёт сам) + * @param serviceClass класс ShizukuVdService наследника (например, VmShizukuService::class.java) + * @param width виртуальный размер дисплея (для очков одного глаза — 640×480) + * @param height то же + * @param density плотность экрана (dpi) + */ + suspend fun acquire( + context: android.content.Context, + surfaceTexture: SurfaceTexture, + serviceClass: Class<*>, + width: Int = 640, + height: Int = 480, + density: Int = 160, + ): VirtualDisplayFrameSource? { + val surface = Surface(surfaceTexture) + val client = ShizukuVdClient(context, serviceClass) + return try { + var binder: IBinder? = null + client.start { b -> binder = b } + val b = binder ?: run { + Log.w(TAG, "Shizuku binder not ready, returning null") + surface.release() + client.stop() + return null + } + val displayId = ShizukuVdBinder.createDisplay( + b, "VmDisplay", width, height, density, surface + ) + if (displayId <= 0) { + Log.w(TAG, "createDisplay returned $displayId") + surface.release() + client.stop() + return null + } + Log.i(TAG, "Acquired VirtualDisplay id=$displayId ${width}x${height}") + VirtualDisplayFrameSource(surfaceTexture, surface, b, displayId, width, height) + } catch (t: Throwable) { + Log.e(TAG, "acquire failed", t) + runCatching { surface.release() } + client.stop() + null + } + } + } +} diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/ui/GlassesScreen.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/ui/GlassesScreen.kt new file mode 100644 index 0000000..b2f4eb2 --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/ui/GlassesScreen.kt @@ -0,0 +1,116 @@ +package pw.binom.rayneovm.glasses.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import pw.binom.mercury.binocular.BinocularRendererView +import pw.binom.mercury.binocular.FrameSource +import pw.binom.mercury.binocular.NoOpFrameSource +import pw.binom.rayneovm.glasses.protocol.VmGlassesController +import pw.binom.rayneovm.glasses.vm.GlassesScreenState + +/** + * Главный экран очков. + * + * Стек из двух слоёв: + * 1. BinocularRendererView (GL SurfaceView) — для состояния [GlassesScreenState.Running] + * 2. Compose overlay — для всех остальных состояний (текст на чёрном фоне) + * + * В [GlassesScreenState.Running] контроллер передаёт настоящий [FrameSource] из VirtualDisplay. + */ +@Composable +fun GlassesScreen( + controller: VmGlassesController, + modifier: Modifier = Modifier, +) { + val state by controller.state.collectAsState() + val frameSource = remember { mutableStateOf(NoOpFrameSource()) } + + Box(modifier = modifier.fillMaxSize().background(Color.Black)) { + // GL-слой: всегда рисуем, но при NoOpFrameSource — просто чёрный экран + BinocularRendererView( + frameSource = frameSource.value, + modifier = Modifier.fillMaxSize(), + ) + + // Compose overlay поверх GL + when (val s = state) { + is GlassesScreenState.Starting -> StatusOverlay("Подготавливаем очки...") + is GlassesScreenState.ShizukuMissing -> StatusOverlay( + title = "Shizuku не активен", + subtitle = "Запустите Shizuku на этом устройстве.\n" + + "Без него приложение не сможет создать виртуальный экран." + ) + is GlassesScreenState.NoConnection -> StatusOverlay( + title = "Нет связи с телефоном", + subtitle = "Убедитесь, что телефон и очки в одной WiFi-сети." + ) + is GlassesScreenState.NoAppSelected -> StatusOverlay( + title = "Выберите приложение на телефоне", + subtitle = "Откройте приложение «RayNeo VM» и выберите приложение для запуска.\n\n[DEBUG] Тап-тест: запустить Zona сейчас", + onClick = { + controller.startTestLaunch("free.zona") + } + ) + is GlassesScreenState.Running -> { + // Ничего поверх GL — пусть пользователь видит гостевой контент. + // FrameSource обновляется в LaunchedEffect ниже. + androidx.compose.runtime.LaunchedEffect(s.packageName) { + val src = controller.acquireFrameSource(s.packageName) + if (src != null) frameSource.value = src + } + } + is GlassesScreenState.Error -> StatusOverlay( + title = "Ошибка", + subtitle = s.message, + ) + } + } +} + +@Composable +private fun StatusOverlay(title: String, subtitle: String? = null, onClick: (() -> Unit)? = null) { + val baseModifier = Modifier.fillMaxSize().background(Color.Black).padding(32.dp) + val modifier = if (onClick != null) baseModifier.clickable { onClick() } else baseModifier + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = title, + color = Color.White, + fontSize = 22.sp, + textAlign = TextAlign.Center, + ) + if (subtitle != null) { + Text( + text = subtitle, + color = Color(0xFFB0B0B0), + fontSize = 14.sp, + textAlign = TextAlign.Center, + ) + } + } + } +} diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/vm/GlassesScreenState.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/vm/GlassesScreenState.kt new file mode 100644 index 0000000..2688547 --- /dev/null +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/vm/GlassesScreenState.kt @@ -0,0 +1,21 @@ +package pw.binom.rayneovm.glasses.vm + +/** + * Состояние экрана очков. + * + * Определяет, что показывать в текущий момент: + * - [Starting] — стартует VmService / Shizuku / VirtualDisplay + * - [ShizukuMissing] — Shizuku не активен + * - [NoConnection] — нет связи с телефоном (mDNS не нашёл нас) + * - [NoAppSelected] — связь есть, но ни одно приложение не выбрано + * - [Running] — VirtualDisplay работает, кадры идут + * - [Error] — фатальная ошибка + */ +sealed interface GlassesScreenState { + data object Starting : GlassesScreenState + data object ShizukuMissing : GlassesScreenState + data object NoConnection : GlassesScreenState + data object NoAppSelected : GlassesScreenState + data class Running(val packageName: String) : GlassesScreenState + data class Error(val message: String) : GlassesScreenState +} diff --git a/app-glasses-vm/src/main/res/values/strings.xml b/app-glasses-vm/src/main/res/values/strings.xml new file mode 100644 index 0000000..9420c84 --- /dev/null +++ b/app-glasses-vm/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + RayNeo VM + diff --git a/app-glasses-vm/src/main/res/values/themes.xml b/app-glasses-vm/src/main/res/values/themes.xml new file mode 100644 index 0000000..08a933d --- /dev/null +++ b/app-glasses-vm/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + diff --git a/app-phone-vm/build.gradle.kts b/app-phone-vm/build.gradle.kts new file mode 100644 index 0000000..55f93f4 --- /dev/null +++ b/app-phone-vm/build.gradle.kts @@ -0,0 +1,80 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "pw.binom.rayneovm.phone" + compileSdk = 37 + + defaultConfig { + applicationId = "pw.binom.rayneovm.phone" + minSdk = 26 + targetSdk = 37 + versionCode = 1 + versionName = "0.1.0" + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } +} + +dependencies { + implementation(project(":shared")) + + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.serialization.json) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons) + implementation(libs.androidx.navigation.compose) + + implementation(libs.shizuku.api) + implementation(libs.mercury.library) + implementation(libs.mercury.transport) + implementation(libs.mercury.binocular) + implementation(libs.mercury.shizuku) +} + +// Установка APK на телефон. +// Запуск: ./gradlew :app-phone-vm:installPhone +tasks.register("installPhone") { + group = "deploy" + description = "Install PhoneApp APK на телефон (serial: AXGL024B05001337)" + dependsOn("assembleDebug") + val serial = "AXGL024B05001337" + val apk = layout.buildDirectory.file("outputs/apk/debug/app-phone-vm-debug.apk") + commandLine("adb", "-s", serial, "install", "-r", apk.get().asFile.absolutePath) +} diff --git a/app-phone-vm/src/main/AndroidManifest.xml b/app-phone-vm/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0578fa0 --- /dev/null +++ b/app-phone-vm/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/MainActivity.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/MainActivity.kt new file mode 100644 index 0000000..45d8825 --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/MainActivity.kt @@ -0,0 +1,59 @@ +package pw.binom.rayneovm.phone + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import pw.binom.rayneovm.phone.mercury.MercuryController +import pw.binom.rayneovm.phone.protocol.VmPhoneController +import pw.binom.rayneovm.phone.ui.HomeScreen + +/** + * Single-Activity приложения. Compose host + Navigation. + */ +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + val controller = VmPhoneController.fromContext(this) + val mercuryController = MercuryController.fromContext(this) + setContent { + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions(), + ) { result -> + val allGranted = result.values.all { it } + mercuryController.onPermissionsResult(allGranted) + } + CompositionLocalProvider( + VmPhoneController.Local provides controller, + MercuryController.Local provides mercuryController, + ) { + MaterialTheme(colorScheme = darkColorScheme()) { + androidx.compose.foundation.layout.Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + ) { + HomeScreen( + controller = controller, + mercuryController = mercuryController, + onRequestMercuryPermissions = { + permissionLauncher.launch(mercuryController.requiredPermissions) + }, + ) + } + } + } + } + } +} diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/PhoneVmApp.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/PhoneVmApp.kt new file mode 100644 index 0000000..96df4c8 --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/PhoneVmApp.kt @@ -0,0 +1,41 @@ +package pw.binom.rayneovm.phone + +import android.app.Application +import android.util.Log +import pw.binom.rayneovm.phone.mercury.MercuryController +import pw.binom.rayneovm.phone.mercury.MercuryControllerProvider +import pw.binom.rayneovm.phone.protocol.ControllerProvider +import pw.binom.rayneovm.phone.protocol.VmPhoneController + +/** + * Application-класс PhoneApp. + * + * При старте подготавливает контроллер [VmPhoneController] (auto-reconnect к очкам через mDNS) + * и [MercuryController] (второй канал — RayNeo Mercury BLE). + */ +class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider { + + private val controller: VmPhoneController by lazy { VmPhoneController(this) } + private val mercuryController: MercuryController by lazy { MercuryController(this) } + + override fun onCreate() { + super.onCreate() + Log.i(TAG, "PhoneVmApp onCreate, uid=${android.os.Process.myUid()}") + // Запуск отложен до первого обращения — в lazy. + controller.start() + mercuryController.refresh() + } + + override fun onTerminate() { + super.onTerminate() + controller.stop() + mercuryController.close() + } + + override fun provideController(): VmPhoneController = controller + override fun provideMercuryController(): MercuryController = mercuryController + + companion object { + private const val TAG = "PhoneVmApp" + } +} diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/audio/AudioPlaybackManager.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/audio/AudioPlaybackManager.kt new file mode 100644 index 0000000..c0120fe --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/audio/AudioPlaybackManager.kt @@ -0,0 +1,108 @@ +package pw.binom.rayneovm.phone.audio + +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioTrack +import android.net.wifi.WifiManager +import android.os.PowerManager +import android.util.Log +import pw.binom.mercury.transport.ByteTransport +import pw.binom.mercury.transport.Transports +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +/** + * Менеджер воспроизведения аудио с очков на телефон (Audio mode 2). + * + * Держит AudioTrack + WiFi/Wake lock + WebSocket-клиент, качающий PCM с очков. + * + * Вызывается из [pw.binom.rayneovm.phone.protocol.VmPhoneController] при получении + * [pw.binom.rayneovm.shared.SetAudioMode] с режимом ON_PHONE. + */ +class AudioPlaybackManager(private val context: Context) { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var track: AudioTrack? = null + private var transport: ByteTransport? = null + private var streamJob: Job? = null + private var wifiLock: WifiManager.WifiLock? = null + private var wakeLock: PowerManager.WakeLock? = null + + fun start(glassesHost: String) { + if (track != null) return // уже запущено + + val sampleRate = SAMPLE_RATE + val channelMask = AudioFormat.CHANNEL_OUT_STEREO + val encoding = AudioFormat.ENCODING_PCM_16BIT + + val minBuffer = AudioTrack.getMinBufferSize(sampleRate, channelMask, encoding) + val bufferSize = minBuffer * 4 + + track = AudioTrack.Builder() + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + .build() + ) + .setAudioFormat( + AudioFormat.Builder() + .setSampleRate(sampleRate) + .setEncoding(encoding) + .setChannelMask(channelMask) + .build() + ) + .setBufferSizeInBytes(bufferSize) + .setTransferMode(AudioTrack.MODE_STREAM) + .build() + track!!.play() + Log.i(TAG, "AudioTrack started: $sampleRate Hz, buffer=$bufferSize") + + // WiFi + Wake lock + val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "VmAudioPlaybackLock").apply { acquire() } + val pm = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "VmAudioPlaybackWakeLock").apply { + acquire(60 * 60 * 1000L) + } + + // Подключаемся к очкам по тому же ws://host:port/vm-audio + val wsUrl = "ws://$glassesHost:${GLASSES_PORT}/vm-audio" + transport = Transports.wifiBytes(wsUrl, autoReconnect = true) + + streamJob = scope.launch { + try { + transport!!.connect( + onMessage = { pcm -> + val t = track ?: return@connect + t.write(pcm, 0, pcm.size) + }, + onConnected = { Log.i(TAG, "audio connected to $wsUrl") }, + onDisconnected = { Log.w(TAG, "audio disconnected") }, + ) + } catch (t: Throwable) { + Log.e(TAG, "audio transport failed", t) + } + } + } + + fun stop() { + streamJob?.cancel(); streamJob = null + transport?.close(); transport = null + track?.stop(); track?.release(); track = null + wifiLock?.release(); wifiLock = null + wakeLock?.release(); wakeLock = null + Log.i(TAG, "AudioPlaybackManager stopped") + } + + companion object { + private const val TAG = "AudioPlaybackMgr" + const val SAMPLE_RATE = 44100 + const val GLASSES_PORT = 8080 + } +} diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/audio/AudioPlaybackService.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/audio/AudioPlaybackService.kt new file mode 100644 index 0000000..0e74a2f --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/audio/AudioPlaybackService.kt @@ -0,0 +1,51 @@ +package pw.binom.rayneovm.phone.audio + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import pw.binom.rayneovm.phone.MainActivity + +/** + * Stub: реальный foreground service — в [AudioPlaybackManager]. + * Класс оставлен для совместимости с AndroidManifest декларацией и не вызывается. + */ +class AudioPlaybackService : Service() { + + override fun onBind(intent: Intent): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startForeground(NOTIFICATION_ID, buildNotification()) + return START_NOT_STICKY + } + + private fun buildNotification(): Notification { + val openApp = PendingIntent.getActivity( + this, 0, Intent(this, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + return NotificationCompat.Builder(this, "vm_audio_playback") + .setSmallIcon(android.R.drawable.ic_media_play) + .setContentTitle("RayNeo VM") + .setContentText("Audio playback активен") + .setContentIntent(openApp) + .setOngoing(true) + .build() + } + + override fun onDestroy() { + super.onDestroy() + } + + companion object { + private const val NOTIFICATION_ID = 300 + } +} diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/mercury/MercuryController.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/mercury/MercuryController.kt new file mode 100644 index 0000000..fa19820 --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/mercury/MercuryController.kt @@ -0,0 +1,271 @@ +package pw.binom.rayneovm.phone.mercury + +import android.Manifest +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.compose.runtime.staticCompositionLocalOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import pw.binom.mercury.DiscoveredGlasses +import pw.binom.mercury.RayNeoMercury + +/** + * Контроллер второго канала связи с очками — RayNeo Mercury BLE. + * + * В отличие от [pw.binom.rayneovm.phone.protocol.VmPhoneController], который работает + * по нашему собственному WebSocket-протоколу поверх WiFi/mDNS, этот контроллер + * прикидывается штатным приложением RayNeo Mercury и говорит с очками по их + * закрытому BLE-протоколу (см. rayneo-mercury/library). + * + * Это bootstrap-канал (правило #4746): нужен, чтобы запустить наше приложение + * на очках через `appMarketLaunch(pkg)`, если mDNS не нашёл WebSocket-сервис. + */ +class MercuryController(private val context: Context) { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val mercury = RayNeoMercury(context, externalScope = scope) + + private val _state = MutableStateFlow(MercuryState.PermissionsMissing) + val state: StateFlow = _state.asStateFlow() + + private var lastScanResults: List = emptyList() + + /** Разрешения, которые нужно запросить для работы с BLE на этом API. */ + val requiredPermissions: Array = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + arrayOf( + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT, + ) + } else { + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) + } + + init { + observeEvents() + refreshPermissionState() + } + + /** Перепроверить состояние Bluetooth-адаптера и разрешений. */ + fun refresh() { + refreshPermissionState() + } + + /** UI вызывает после системного диалога разрешений. */ + fun onPermissionsResult(granted: Boolean) { + if (granted) { + if (isBluetoothEnabled()) { + _state.value = MercuryState.Disconnected() + } else { + _state.value = MercuryState.BluetoothOff + } + } else { + _state.value = MercuryState.PermissionsMissing + } + } + + /** + * Сканировать ближайшие очки (RayNeo Mercury advertisement). + * По окончании — переход в [MercuryState.Disconnected] с заполненным + * `lastScanResults`, или в [MercuryState.Scanning] во время скана. + */ + fun scan() { + if (!hasPermissions()) { + _state.value = MercuryState.PermissionsMissing + return + } + if (!isBluetoothEnabled()) { + _state.value = MercuryState.BluetoothOff + return + } + _state.value = MercuryState.Scanning(emptyList()) + scope.launch { + val results = mutableListOf() + try { + mercury.scan()?.collect { glasses -> + if (results.none { it.address == glasses.address }) { + results += glasses + _state.value = MercuryState.Scanning(results.toList()) + } + } ?: run { + _state.value = MercuryState.Disconnected( + message = "Bluetooth-адаптер недоступен", + ) + return@launch + } + // Скан завершился без явной отмены — фиксируем результат + lastScanResults = results + if (results.isEmpty()) { + _state.value = MercuryState.Disconnected( + message = "Очки не найдены. Включите их и поднесите ближе.", + ) + } else { + _state.value = MercuryState.Disconnected( + message = "Найдено устройств: ${results.size}. Выберите для подключения.", + ) + } + } catch (t: Throwable) { + _state.value = MercuryState.Disconnected(message = "Ошибка скана: ${t.message}") + } + } + } + + /** + * Подключиться к найденному устройству. Если оно ещё не спарено — сначала + * создаём OS-level bond, потом открываем GATT-сессию. + */ + fun connect(glasses: DiscoveredGlasses) { + if (!hasPermissions()) { + _state.value = MercuryState.PermissionsMissing + return + } + _state.value = MercuryState.Connecting(glasses) + scope.launch { + try { + val paired = mercury.pair(glasses) + if (paired is pw.binom.mercury.pair.PairManager.PairingResult.Failed) { + _state.value = MercuryState.Disconnected( + message = "Сопряжение не удалось: ${paired.reason}", + ) + return@launch + } + val ok = mercury.connect(glasses) + if (!ok) { + _state.value = MercuryState.Disconnected( + message = "Не удалось открыть GATT-сессию", + ) + return@launch + } + _state.value = MercuryState.Connected(glasses, batteryPercent = null) + // Запрашиваем актуальный статус, чтобы получить battery/wifi + mercury.pullGeneralStatus() + } catch (t: Throwable) { + _state.value = MercuryState.Disconnected(message = "Ошибка: ${t.message}") + } + } + } + + fun disconnect() { + scope.launch { mercury.disconnect() } + _state.value = MercuryState.Disconnected() + } + + /** + * Запустить приложение на очках через штатный механизм Mercury + * (appMarketLaunch). Требует активной BLE-сессии — иначе вернёт false. + */ + suspend fun launchGlassesApp(packageName: String): Boolean { + if (_state.value !is MercuryState.Connected) return false + return mercury.appMarketLaunch(packageName) + } + + /** Доступ к raw event-flow библиотеки (debug/диагностика). */ + val events get() = mercury.events + + private fun refreshPermissionState() { + _state.value = when { + !hasPermissions() -> MercuryState.PermissionsMissing + !isBluetoothEnabled() -> MercuryState.BluetoothOff + else -> if (_state.value is MercuryState.Connected) _state.value else MercuryState.Disconnected() + } + } + + private fun hasPermissions(): Boolean = requiredPermissions.all { + context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED + } + + private fun isBluetoothEnabled(): Boolean { + val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter + return try { + adapter?.isEnabled == true + } catch (_: SecurityException) { + false + } + } + + private fun observeEvents() { + scope.launch { + mercury.events.collect { event -> + when (event) { + is RayNeoMercury.Event.Connected -> { + val cur = _state.value + if (cur is MercuryState.Connecting) { + _state.value = MercuryState.Connected(cur.device, batteryPercent = null) + } + } + is RayNeoMercury.Event.Disconnected -> { + val cur = _state.value + if (cur is MercuryState.Connected) { + _state.value = MercuryState.Disconnected( + message = "Связь потеряна: ${event.cause}", + ) + } + } + is RayNeoMercury.Event.GeneralStatusUpdate -> { + val cur = _state.value + if (cur is MercuryState.Connected) { + _state.value = cur.copy( + batteryPercent = event.status.battery, + ) + } + } + else -> Unit + } + } + } + } + + fun close() { + scope.coroutineContext[Job]?.cancel() + mercury.close() + } + + companion object { + val Local = staticCompositionLocalOf { + error("MercuryController not provided") + } + + fun fromContext(context: Context): MercuryController = + (context.applicationContext as? MercuryControllerProvider)?.provideMercuryController() + ?: error("Application must implement MercuryControllerProvider") + } +} + +interface MercuryControllerProvider { + fun provideMercuryController(): MercuryController +} + +/** + * Состояние второго канала (Mercury BLE). UI рендерит это как «Канал 2». + */ +sealed interface MercuryState { + /** Нет разрешений BLUETOOTH_SCAN / BLUETOOTH_CONNECT. */ + data object PermissionsMissing : MercuryState + + /** Разрешения есть, но Bluetooth выключен. */ + data object BluetoothOff : MercuryState + + /** Готовы к сканированию / подключению. */ + data class Disconnected(val message: String? = null) : MercuryState + + /** Идёт скан, [found] — то, что уже нашли. */ + data class Scanning(val found: List) : MercuryState + + /** Устанавливаем пару + GATT. */ + data class Connecting(val device: DiscoveredGlasses) : MercuryState + + /** GATT-сессия жива, очки отвечают. */ + data class Connected( + val device: DiscoveredGlasses, + val batteryPercent: Int? = null, + ) : MercuryState +} \ No newline at end of file diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/protocol/VmPhoneController.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/protocol/VmPhoneController.kt new file mode 100644 index 0000000..ea8c595 --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/protocol/VmPhoneController.kt @@ -0,0 +1,191 @@ +package pw.binom.rayneovm.phone.protocol + +import android.content.Context +import androidx.compose.runtime.staticCompositionLocalOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import pw.binom.mercury.transport.StringTransport +import pw.binom.mercury.transport.Transports +import pw.binom.rayneovm.phone.audio.AudioPlaybackManager +import pw.binom.rayneovm.shared.AppCrashedEvent +import pw.binom.rayneovm.shared.AppLaunchedAck +import pw.binom.rayneovm.shared.GlassesStatus +import pw.binom.rayneovm.shared.Pong +import pw.binom.rayneovm.shared.SetAudioMode +import pw.binom.rayneovm.shared.VmMessage +import pw.binom.rayneovm.shared.VmProtocol + +/** + * Контроллер состояния и связи с очками на стороне телефона. + * + * Запускает фоновый цикл авто-реконнекта к очкам через mDNS + WebSocket. + * Хранит текущее состояние связи и обрабатывает входящие/исходящие сообщения. + */ +class VmPhoneController( + @Suppress("unused") private val context: Context, +) { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + private val _state = MutableStateFlow(ConnectionState.Searching) + val state: StateFlow = _state.asStateFlow() + + private val _glassesStatus = MutableStateFlow(null) + val glassesStatus: StateFlow = _glassesStatus.asStateFlow() + + private var currentTransport: StringTransport? = null + private val audioManager by lazy { AudioPlaybackManager(context) } + + fun start() { + if (scope.coroutineContext[Job]?.children?.any { it.isActive } == true) return + scope.launch { reconnectLoop() } + } + + fun stop() { + currentTransport?.close() + currentTransport = null + audioManager.stop() + scope.coroutineContext[Job]?.cancelChildren() + _state.value = ConnectionState.Disconnected + } + + /** + * Цикл поиска очков через mDNS + подключения. + * + * При разрыве — exponential backoff. + */ + private suspend fun reconnectLoop() { + var attempt = 0 + val backoffMs = listOf(1_000L, 2_000L, 4_000L, 8_000L, 16_000L, 30_000L) + while (true) { + try { + _state.value = ConnectionState.Searching + val scanner = pw.binom.mercury.transport.MdnsScanner(context, "_rayneo-vm._tcp") + // mDNS пытается 5с; если не вышло — TCP-фоллбек по подсети ещё ~5с. + // Порт 8080 (см. docs/SPEC.md:4752 и VmService.WS_PORT). + val service = scanner.discoverOnce(timeoutMs = 10_000, fallbackPort = 8080) + _state.value = ConnectionState.Connecting(service.host) + val url = "ws://${service.host}:${service.port}/vm" + val transport = Transports.wifi(url, autoReconnect = false) + currentTransport = transport + _state.value = ConnectionState.Connected(service.host) + attempt = 0 + transport.connect( + onMessage = { json -> + val msg = try { + VmProtocol.decodeAny(json) + } catch (_: Throwable) { return@connect } + handleIncoming(msg) + }, + onConnected = { /* уже выставлено в Connecting->Connected */ }, + onDisconnected = { + currentTransport = null + _state.value = ConnectionState.Disconnected + }, + ) + } catch (t: Throwable) { + currentTransport = null + _state.value = ConnectionState.Disconnected + } + val delay = backoffMs[attempt.coerceAtMost(backoffMs.lastIndex)] + attempt++ + kotlinx.coroutines.delay(delay) + } + } + + private fun handleIncoming(msg: VmMessage) { + when (msg) { + is GlassesStatus -> { + _glassesStatus.value = GlassesStatusInfo( + batteryPercent = msg.batteryPercent, + wifiSsid = msg.wifiSsid, + currentApp = msg.currentApp, + running = msg.running, + ) + } + is AppCrashedEvent -> { + _appCrashed.value = msg.packageName + } + is AppLaunchedAck -> { + _appLaunched.value = msg + } + is SetAudioMode -> { + // Аудио режим — отдельный поток, не контролируется этим контроллером напрямую. + // AudioCaptureService на очках стартует/останавливается. + // На телефоне режим управляется UI (toggle на главном экране). + } + is Pong -> { /* keepalive */ } + else -> { /* другие: событийные, форвардим */ } + } + } + + private val _appCrashed = MutableStateFlow(null) + val appCrashed: StateFlow = _appCrashed.asStateFlow() + + private val _appLaunched = MutableStateFlow(null) + val appLaunched: StateFlow = _appLaunched.asStateFlow() + + fun clearAppCrashed() { _appCrashed.value = null } + fun clearAppLaunched() { _appLaunched.value = null } + + /** Отправляет сообщение очкам. */ + suspend fun send(msg: VmMessage) { + val t = currentTransport ?: return + try { + t.send(VmProtocol.encode(msg)) + } catch (t: Throwable) { + // ignore — connection state управляется через reconnect loop + } + } + + /** + * Переключает аудио режим: 0 = ON_GLASSES, 1 = ON_PHONE. + * Шлёт SetAudioMode очкам, и на телефоне запускает/останавливает AudioPlaybackManager. + */ + suspend fun setAudioMode(mode: pw.binom.rayneovm.shared.AudioMode) { + send(SetAudioMode(mode)) + when (mode) { + pw.binom.rayneovm.shared.AudioMode.ON_GLASSES -> audioManager.stop() + pw.binom.rayneovm.shared.AudioMode.ON_PHONE -> { + val host = (currentTransport as? pw.binom.mercury.transport.WifiTransport)?.let { null } + ?: (_state.value as? ConnectionState.Connected)?.host + if (host != null) audioManager.start(host) + } + } + } + + companion object { + val Local = staticCompositionLocalOf { + error("VmPhoneController not provided") + } + + fun fromContext(context: Context): VmPhoneController = + (context.applicationContext as? ControllerProvider)?.provideController() + ?: error("Application must implement ControllerProvider") + } +} + +interface ControllerProvider { + fun provideController(): VmPhoneController +} + +sealed interface ConnectionState { + data object Searching : ConnectionState + data class Connecting(val host: String) : ConnectionState + data class Connected(val host: String) : ConnectionState + data object Disconnected : ConnectionState +} + +data class GlassesStatusInfo( + val batteryPercent: Int?, + val wifiSsid: String?, + val currentApp: String?, + val running: Boolean, +) diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/AppsListScreen.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/AppsListScreen.kt new file mode 100644 index 0000000..ce311f6 --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/AppsListScreen.kt @@ -0,0 +1,105 @@ +package pw.binom.rayneovm.phone.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch +import pw.binom.rayneovm.phone.protocol.VmPhoneController +import pw.binom.rayneovm.shared.LaunchAppCommand + +/** + * Список приложений, которые можно запустить на очках через VirtualDisplay. + * + * Сейчас — только Zona. В будущем: реестр в shared/AppProfiles.kt. + */ +@Composable +fun AppsListScreen( + controller: VmPhoneController, + onAppClick: (String) -> Unit = {}, +) { + val glasses by controller.glassesStatus.collectAsState() + val isRunning = glasses?.running == true + val coroutineScope = androidx.compose.runtime.rememberCoroutineScope() + + val apps = listOf( + AppEntry("free.zona", "Zona", "Кинотеатр Zona"), + ) + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = "Приложения", + color = Color.White, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + ) + Text( + text = "Тапните, чтобы запустить на очках", + color = Color(0xFFB0B0B0), + fontSize = 14.sp, + ) + + apps.forEach { app -> + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { + coroutineScope.launch { + controller.send(LaunchAppCommand(packageName = app.packageName)) + } + onAppClick(app.packageName) + }, + colors = androidx.compose.material3.CardDefaults.cardColors( + containerColor = Color(0xFF1A1A1A), + ), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = app.name, + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = app.description, + color = Color(0xFFB0B0B0), + fontSize = 12.sp, + ) + if (isRunning && glasses?.currentApp == app.packageName) { + Text( + text = "● запущено", + color = Color(0xFF4CAF50), + fontSize = 12.sp, + ) + } + } + } + } + } +} + +private data class AppEntry( + val packageName: String, + val name: String, + val description: String, +) diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/GlassesStatusScreen.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/GlassesStatusScreen.kt new file mode 100644 index 0000000..3b86eb0 --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/GlassesStatusScreen.kt @@ -0,0 +1,263 @@ +package pw.binom.rayneovm.phone.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch +import pw.binom.mercury.DiscoveredGlasses +import pw.binom.rayneovm.phone.mercury.MercuryController +import pw.binom.rayneovm.phone.mercury.MercuryState +import pw.binom.rayneovm.phone.protocol.ConnectionState +import pw.binom.rayneovm.phone.protocol.VmPhoneController + +/** + * Экран «Очки»: два независимых канала связи + запуск нашего приложения. + * + * - **Канал 1 (WebSocket)** — наше приложение на очках подключается к нам по WiFi/mDNS. + * Это основной control-канал (тач, клавиши, команды). + * - **Канал 2 (Mercury BLE)** — мы прикидываемся штатным приложением RayNeo Mercury + * и общаемся с очками по их закрытому BLE-протоколу. Это bootstrap-канал: + * нужен, чтобы запустить наше приложение через `appMarketLaunch(pkg)`, когда + * очки ещё не подключены к нам по WebSocket. + * + * Кнопка «Запустить наше приложение на очках» работает только когда жив Канал 2. + */ +@Composable +fun GlassesStatusScreen( + controller: VmPhoneController, + mercuryController: MercuryController, + onRequestMercuryPermissions: () -> Unit = {}, +) { + val connection by controller.state.collectAsState() + val glasses by controller.glassesStatus.collectAsState() + val mercury by mercuryController.state.collectAsState() + val scope = rememberCoroutineScope() + + val mercuryConnected = mercury is MercuryState.Connected + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + .verticalScroll(rememberScrollState()) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = "Очки", + color = Color.White, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + ) + + Channel1WebSocketSection(connection = connection, glasses = glasses) + + HorizontalDivider(color = Color(0xFF2A2A2A)) + + Channel2MercurySection( + mercury = mercury, + onRequestPermissions = onRequestMercuryPermissions, + onScan = { mercuryController.scan() }, + onConnect = { mercuryController.connect(it) }, + onDisconnect = { mercuryController.disconnect() }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + LaunchGlassesAppButton( + enabled = mercuryConnected, + onClick = { + scope.launch { + mercuryController.launchGlassesApp(GLASSES_PACKAGE) + } + }, + ) + } +} + +private const val GLASSES_PACKAGE = "pw.binom.rayneovm.glasses" + +@Composable +private fun Channel1WebSocketSection( + connection: ConnectionState, + glasses: pw.binom.rayneovm.phone.protocol.GlassesStatusInfo?, +) { + SectionCard(title = "Канал 1 — наше приложение (WebSocket)") { + StatusRow("Связь", connection.label) + when (connection) { + is ConnectionState.Connected -> glasses?.let { g -> + StatusRow("Аккумулятор", g.batteryPercent?.let { "$it%" } ?: "—") + StatusRow("WiFi", g.wifiSsid ?: "—") + StatusRow("Запущено", g.currentApp ?: "—") + StatusRow("Активно", if (g.running) "да" else "нет") + } ?: run { + Hint("Ожидаем статус от очков…") + } + else -> Hint( + when (connection) { + is ConnectionState.Searching -> "Ищем очки в сети…" + is ConnectionState.Connecting -> "Подключаемся к ${connection.host}…" + is ConnectionState.Disconnected -> + "Нет связи. Проверьте, что очки и телефон в одной WiFi-сети." + } + ) + } + } +} + +@Composable +private fun Channel2MercurySection( + mercury: MercuryState, + onRequestPermissions: () -> Unit, + onScan: () -> Unit, + onConnect: (DiscoveredGlasses) -> Unit, + onDisconnect: () -> Unit, +) { + SectionCard(title = "Канал 2 — очки (Mercury BLE)") { + when (mercury) { + is MercuryState.PermissionsMissing -> { + Hint("Нужны разрешения Bluetooth (BLUETOOTH_SCAN, BLUETOOTH_CONNECT).") + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = onRequestPermissions) { Text("Запросить разрешения") } + } + is MercuryState.BluetoothOff -> { + Hint("Bluetooth выключен. Включите его в системных настройках.") + Spacer(modifier = Modifier.height(8.dp)) + OutlinedButton(onClick = onScan) { Text("Проверить ещё раз") } + } + is MercuryState.Disconnected -> { + StatusRow("Связь", "нет связи") + mercury.message?.let { Hint(it) } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = onScan) { Text("Сканировать") } + } + is MercuryState.Scanning -> { + StatusRow("Связь", "поиск… (найдено ${mercury.found.size})") + Spacer(modifier = Modifier.height(8.dp)) + if (mercury.found.isNotEmpty()) { + Text("Найденные устройства:", color = Color(0xFFB0B0B0), fontSize = 12.sp) + mercury.found.forEach { g -> + DeviceRow(g, onClick = { onConnect(g) }) + } + } else { + Hint("Идёт скан, подождите…") + } + } + is MercuryState.Connecting -> { + StatusRow("Связь", "сопрягаем ${mercury.device.name ?: mercury.device.address}…") + Hint("Если очки запрашивают PIN — подтвердите на них.") + } + is MercuryState.Connected -> { + StatusRow("Связь", "подключено (${mercury.device.name ?: mercury.device.address})") + StatusRow("Адрес", mercury.device.address) + StatusRow("Аккумулятор", mercury.batteryPercent?.let { "$it%" } ?: "—") + Spacer(modifier = Modifier.height(8.dp)) + OutlinedButton(onClick = onDisconnect) { Text("Отключить") } + } + } + } +} + +@Composable +private fun LaunchGlassesAppButton(enabled: Boolean, onClick: () -> Unit) { + Button( + onClick = onClick, + enabled = enabled, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF1565C0), + disabledContainerColor = Color(0xFF2A2A2A), + ), + ) { + Text(if (enabled) "Запустить наше приложение на очках" else "Запустить наше приложение на очках (нужен Канал 2)") + } +} + +@Composable +private fun SectionCard(title: String, content: @Composable () -> Unit) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color(0xFF141414)), + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = title, + color = Color(0xFF8AB4F8), + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ) + content() + } + } +} + +@Composable +private fun DeviceRow(glasses: DiscoveredGlasses, onClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column { + Text(glasses.name ?: "—", color = Color.White, fontSize = 14.sp) + Text(glasses.address, color = Color(0xFFB0B0B0), fontSize = 11.sp) + } + Button(onClick = onClick) { Text("Подключить") } + } +} + +@Composable +private fun StatusRow(label: String, value: String) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = label, color = Color(0xFFB0B0B0), fontSize = 14.sp) + Text(text = value, color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Medium) + } +} + +@Composable +private fun Hint(text: String) { + Box(modifier = Modifier.padding(top = 4.dp)) { + Text(text = text, color = Color(0xFFB0B0B0), fontSize = 13.sp) + } +} + +private val ConnectionState.label: String + get() = when (this) { + is ConnectionState.Searching -> "поиск…" + is ConnectionState.Connecting -> "подключение…" + is ConnectionState.Connected -> "подключено ($host)" + is ConnectionState.Disconnected -> "нет связи" + } \ No newline at end of file diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/HomeScreen.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/HomeScreen.kt new file mode 100644 index 0000000..a0c4f58 --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/HomeScreen.kt @@ -0,0 +1,75 @@ +package pw.binom.rayneovm.phone.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import pw.binom.rayneovm.phone.mercury.MercuryController +import pw.binom.rayneovm.phone.protocol.VmPhoneController + +/** + * Главный экран приложения: NavigationBar с двумя табами. + * + * - «Очки» — статус связи и состояние GlassesApp + * - «Приложения» — список приложений для запуска + */ +@Composable +fun HomeScreen( + controller: VmPhoneController, + mercuryController: MercuryController, + onRequestMercuryPermissions: () -> Unit = {}, +) { + var selectedTab by remember { mutableStateOf(Tab.GLASSES) } + + MaterialTheme(colorScheme = darkColorScheme()) { + Scaffold( + modifier = Modifier.fillMaxSize().background(Color.Black), + containerColor = Color.Black, + bottomBar = { + NavigationBar(containerColor = Color(0xFF101010)) { + Tab.entries.forEach { tab -> + NavigationBarItem( + selected = tab == selectedTab, + onClick = { selectedTab = tab }, + icon = { Icon(tab.icon, contentDescription = tab.label) }, + label = { Text(tab.label) }, + ) + } + } + } + ) { padding -> + Box(modifier = Modifier.fillMaxSize().padding(padding)) { + when (selectedTab) { + Tab.GLASSES -> GlassesStatusScreen( + controller = controller, + mercuryController = mercuryController, + onRequestMercuryPermissions = onRequestMercuryPermissions, + ) + Tab.APPS -> AppsListScreen(controller = controller) + } + } + } + } +} + +private enum class Tab(val label: String, val icon: androidx.compose.ui.graphics.vector.ImageVector) { + GLASSES("Очки", Icons.Default.Settings), + APPS("Приложения", Icons.Default.Apps), +} diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/TouchpadScreen.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/TouchpadScreen.kt new file mode 100644 index 0000000..451ed42 --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/TouchpadScreen.kt @@ -0,0 +1,107 @@ +package pw.binom.rayneovm.phone.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import pw.binom.rayneovm.phone.protocol.VmPhoneController +import pw.binom.rayneovm.shared.TouchAction +import pw.binom.rayneovm.shared.TouchEvent +import pw.binom.rayneovm.shared.TouchType +import kotlin.math.roundToInt + +/** + * Виртуальный тачпад: тапы/перетаскивания → [TouchEvent] на очки. + * + * Координаты внутри тачпада нормализуются в [0, targetWidth-1] × [0, targetHeight-1]. + * Точка касания визуализируется кружком. + */ +@Composable +fun TouchpadScreen( + controller: VmPhoneController, + targetWidth: Int = 640, + targetHeight: Int = 480, + modifier: Modifier = Modifier, +) { + val scope = rememberCoroutineScope() + var touchPoint by remember { mutableStateOf(null) } + + Box( + modifier = modifier + .background(Color(0xFF101010)) + .pointerInput(targetWidth, targetHeight) { + detectTapGestures( + onPress = { offset -> + val x = (offset.x / size.width * targetWidth).roundToInt().coerceIn(0, targetWidth - 1) + val y = (offset.y / size.height * targetHeight).roundToInt().coerceIn(0, targetHeight - 1) + touchPoint = offset + scope.launch { + controller.send(TouchEvent(actions = listOf(TouchAction(TouchType.DOWN, x.toFloat(), y.toFloat())))) + try { tryAwaitRelease() } finally { + touchPoint = null + } + controller.send(TouchEvent(actions = listOf(TouchAction(TouchType.UP, x.toFloat(), y.toFloat())))) + } + }, + ) + } + .pointerInput(targetWidth, targetHeight) { + detectDragGestures( + onDragStart = { offset -> + val x = (offset.x / size.width * targetWidth).roundToInt().coerceIn(0, targetWidth - 1) + val y = (offset.y / size.height * targetHeight).roundToInt().coerceIn(0, targetHeight - 1) + touchPoint = offset + scope.launch { + controller.send(TouchEvent(actions = listOf(TouchAction(TouchType.DOWN, x.toFloat(), y.toFloat())))) + } + }, + onDragEnd = { + touchPoint?.let { last -> + val x = (last.x / size.width * targetWidth).roundToInt().coerceIn(0, targetWidth - 1) + val y = (last.y / size.height * targetHeight).roundToInt().coerceIn(0, targetHeight - 1) + scope.launch { + controller.send(TouchEvent(actions = listOf(TouchAction(TouchType.UP, x.toFloat(), y.toFloat())))) + } + } + touchPoint = null + }, + onDragCancel = { touchPoint = null }, + onDrag = { change, _ -> + val x = (change.position.x / size.width * targetWidth).roundToInt().coerceIn(0, targetWidth - 1) + val y = (change.position.y / size.height * targetHeight).roundToInt().coerceIn(0, targetHeight - 1) + touchPoint = change.position + scope.launch { + controller.send(TouchEvent(actions = listOf(TouchAction(TouchType.MOVE, x.toFloat(), y.toFloat())))) + } + }, + ) + }, + ) { + touchPoint?.let { pos -> + Box( + modifier = Modifier + .offset { IntOffset((pos.x - 24.dp.toPx()).toInt(), (pos.y - 24.dp.toPx()).toInt()) } + .size(48.dp) + .clip(CircleShape) + .background(Color(0xFF80CBC4).copy(alpha = 0.6f)) + ) + } + } +} diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/ZonaProfile.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/ZonaProfile.kt new file mode 100644 index 0000000..985e63a --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/ui/ZonaProfile.kt @@ -0,0 +1,196 @@ +package pw.binom.rayneovm.phone.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Keyboard +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material.icons.filled.SkipPrevious +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.text.KeyboardOptions +import kotlinx.coroutines.launch +import pw.binom.rayneovm.phone.protocol.VmPhoneController +import pw.binom.rayneovm.shared.KeyEvent as VmKeyEvent +import pw.binom.rayneovm.shared.LaunchAppCommand + +/** + * Профиль Zona: тачпад + медиа-кнопки + клавиатура + назад. + */ +@Composable +fun ZonaProfile( + controller: VmPhoneController, + modifier: Modifier = Modifier, +) { + val scope = rememberCoroutineScope() + val glasses by controller.glassesStatus.collectAsState() + val appCrashed by controller.appCrashed.collectAsState() + var showKeyboard by remember { mutableStateOf(false) } + + // Если приложение упало — сразу назад (если хочется — можно показать Snackbar) + androidx.compose.runtime.LaunchedEffect(appCrashed) { + if (appCrashed != null) { + // TODO: показать Snackbar и попап обратно в список + controller.clearAppCrashed() + } + } + + Column(modifier = modifier.fillMaxSize().background(Color.Black)) { + // Верхняя панель: назад + название + статус + Row( + modifier = Modifier.fillMaxWidth().padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = { /* TODO: navigateBack */ }) { + Icon(Icons.Default.ArrowBack, contentDescription = "Назад", tint = Color.White) + } + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Zona", + color = Color.White, + fontSize = 18.sp, + ) + Spacer(modifier = Modifier.weight(1f)) + if (glasses?.currentApp == "free.zona") { + Text( + text = if (glasses?.running == true) "●" else "○", + color = if (glasses?.running == true) Color(0xFF4CAF50) else Color(0xFFB0B0B0), + fontSize = 20.sp, + ) + } + } + + // Тачпад (занимает большую часть экрана) + TouchpadScreen( + controller = controller, + modifier = Modifier.weight(1f).fillMaxWidth().padding(8.dp), + ) + + // Нижняя панель: медиа-кнопки + клавиатура + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = { + scope.launch { controller.send(VmKeyEvent(keyCode = 89, action = 1)) } // KEYCODE_MEDIA_REWIND + }) { Icon(Icons.Default.SkipPrevious, contentDescription = "Назад", tint = Color.White) } + + IconButton(onClick = { + scope.launch { controller.send(VmKeyEvent(keyCode = 85, action = 1)) } // KEYCODE_MEDIA_PLAY_PAUSE + }) { Icon(Icons.Default.PlayArrow, contentDescription = "Play/Pause", tint = Color.White) } + + IconButton(onClick = { + scope.launch { controller.send(VmKeyEvent(keyCode = 90, action = 1)) } // KEYCODE_MEDIA_FAST_FORWARD + }) { Icon(Icons.Default.SkipNext, contentDescription = "Вперёд", tint = Color.White) } + + IconButton(onClick = { + scope.launch { controller.send(VmKeyEvent(keyCode = 4, action = 1)) } // KEYCODE_BACK + }) { Icon(Icons.Default.ArrowBack, contentDescription = "Назад (в приложении)", tint = Color.White) } + + IconButton(onClick = { showKeyboard = true }) { + Icon(Icons.Default.Keyboard, contentDescription = "Клавиатура", tint = Color.White) + } + } + } + + if (showKeyboard) { + KeyboardDialog( + onDismiss = { showKeyboard = false }, + onSend = { text -> + scope.launch { + text.forEach { ch -> + val keyCode = keyCodeForChar(ch) + if (keyCode != -1) { + controller.send(VmKeyEvent(keyCode = keyCode, action = 1)) + } + } + } + showKeyboard = false + }, + ) + } +} + +@Composable +private fun KeyboardDialog( + onDismiss: () -> Unit, + onSend: (String) -> Unit, +) { + var input by remember { mutableStateOf("") } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Ввод текста") }, + text = { + OutlinedTextField( + value = input, + onValueChange = { input = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done, + ), + ) + }, + confirmButton = { + TextButton(onClick = { onSend(input) }) { Text("Отправить") } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text("Отмена") } + }, + ) +} + +/** + * ASCII char → Android keyCode. Простая таблица, без юникода и без спец.клавиш. + * Возвращает -1 для символов, которые нельзя передать через KEYCODE. + */ +private fun keyCodeForChar(ch: Char): Int { + return when (ch) { + in 'a'..'z' -> android.view.KeyEvent.KEYCODE_A + (ch - 'a') + in 'A'..'Z' -> android.view.KeyEvent.KEYCODE_A + (ch - 'A') + in '0'..'9' -> android.view.KeyEvent.KEYCODE_0 + (ch - '0') + ' ' -> android.view.KeyEvent.KEYCODE_SPACE + '\n' -> android.view.KeyEvent.KEYCODE_ENTER + '.' -> android.view.KeyEvent.KEYCODE_PERIOD + ',' -> android.view.KeyEvent.KEYCODE_COMMA + '!' -> android.view.KeyEvent.KEYCODE_1 // shift+1 — отдаём как KEYCODE_1 + shift, но тут упрощённо + '?' -> android.view.KeyEvent.KEYCODE_SLASH + else -> -1 + } +} + +@Composable +private fun Spacer(modifier: Modifier) { + androidx.compose.foundation.layout.Spacer(modifier = modifier) +} diff --git a/app-phone-vm/src/main/res/values/strings.xml b/app-phone-vm/src/main/res/values/strings.xml new file mode 100644 index 0000000..9420c84 --- /dev/null +++ b/app-phone-vm/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + RayNeo VM + diff --git a/app-phone-vm/src/main/res/values/themes.xml b/app-phone-vm/src/main/res/values/themes.xml new file mode 100644 index 0000000..08a933d --- /dev/null +++ b/app-phone-vm/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..545244a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,7 @@ +// Top-level build file +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.compose) apply false +} diff --git a/docs/SPEC.md b/docs/SPEC.md new file mode 100644 index 0000000..90e1081 --- /dev/null +++ b/docs/SPEC.md @@ -0,0 +1,1369 @@ +# ТЗ: rayneo-vm — запуск произвольного приложения на очках RayNeo X3 Pro + +> **Статус документа:** черновик (v0.6). Будет уточняться по ходу разработки. + +--- + +## 1. Цель + +Сделать систему из **двух подмодулей** + **общей либы**, которая позволяет запускать произвольное Android-приложение (например, онлайн-кинотеатр **Zona**) на умных очках **RayNeo X3 Pro** и управлять им с телефона. + +### 1.1. Подмодули + +| Подмодуль | Где работает | Что делает | +|---|---|---| +| **`app-glasses-vm`** (GlassesApp) | на очках RayNeo X3 Pro | Создаёт `VirtualDisplay` 640×480, запускает на нём «гостевое» приложение, захватывает кадры и рисует их в обе половины физического дисплея очков (1280×480). Принимает команды управления от телефона. | +| **`app-phone-vm`** (PhoneApp) | на обычном Android-телефоне | UI с тачпадом и кнопкой клавиатуры. Передаёт события тачпада и клавиатуры на очки. | + +### 1.2. Общая либа `rayneo-mercury` + +Переиспользуемые компоненты живут как **новые модули** внутри существующей либы `/home/subochev/common-projects/WORK/rayneo-mercury`: + +| Модуль | Что внутри | +|---|---| +| `:transport` | Канал связи телефон ↔ очки: WebSocket по WiFi, Bluetooth fallback, mDNS-дискавери, общий протокол сообщений. Переносим из view-mate/lib-core (адаптируем namespace и зависимости). | +| `:binocular-renderer` | Обобщённый бинокулярный EGL-рендерер. Принимает «источник кадров» (интерфейс) и рисует его в обе половины экрана очков. Источник может быть видеоплеер (view-mate) или `SurfaceTexture` от VirtualDisplay (rayneo-vm). | + +view-mate ничего не правим — другой агент мигрирует его на новые модули позже. + +--- + +## 2. Термины + +| Термин | Значение | +|---|---| +| **Glasses** | RayNeo X3 Pro (модель ARGF20, Android 12, API 32). | +| **Phone** | Обычный Android-телефон (компаньон). | +| **GlassesApp** | Подмодуль `app-glasses-vm` — приложение, работающее на очках. | +| **PhoneApp** | Подмодуль `app-phone-vm` — приложение, работающее на телефоне. | +| **VM-сервис** | Shizuku UserService внутри GlassesApp — в нём делаем всю работу с системными API (создание VirtualDisplay, запуск Activity). | +| **Виртуальный экран (VirtualDisplay)** | Программно созданный Android-дисплей 640×480 landscape, на котором работает «гостевое» приложение. | +| **Гостевое приложение** | `free.zona` — онлайн-кинотеатр, который мы запускаем на VirtualDisplay. | +| **Бинокулярный вывод** | Отображение одной и той же картинки дважды: левая половина → левый глаз, правая половина → правый глаз. | +| **Канал связи** | WebSocket-соединение между GlassesApp и PhoneApp (WiFi) с Bluetooth-фоллбеком. | +| **binocular-renderer** | Общий модуль в rayneo-mercury, умеет рисовать произвольный источник кадров в обе половины экрана очков. | + +--- + +## 3. Архитектура + +``` + Телефон (PhoneApp) Очки (GlassesApp) + ┌─────────────────────┐ ┌──────────────────────────┐ + │ │ WebSocket (WiFi) │ MainActivity │ + │ Тачпад (тапы/ │◄──────────────────────►│ (binocular-renderer) │ + │ свайпы/жесты) │ BT (fallback) │ │ + │ │ │ ┌────────────────────┐ │ + │ Кнопка «клавиатура»│ │ │ Shizuku UserService│ │ + │ → системная │ │ │ │ │ + │ клавиатура │ │ │ • IDisplayManager │ │ + │ │ │ │ .createVirtual │ │ + │ Статус-индикатор: │ │ │ Display(...) │ │ + │ связь/батарея/ │ │ │ • IActivityTaskMgr │ │ + │ состояние │ │ │ .startActivity │ │ + │ │ │ │ AsUser(...) │ │ + └─────────────────────┘ │ └────────┬───────────┘ │ + │ │ │ + │ ▼ │ + │ VirtualDisplay 640×480 │ + │ ┌────────────────────┐ │ + │ │ free.zona │ │ + │ │ (кинотеатр) │ │ + │ └────────────────────┘ │ + │ │ │ + │ Surface кадров │ + │ ▼ │ + │ binocular-renderer │ + │ (текстура → 2 копии │ + │ в 1280×480) │ + └──────────────────────────┘ + │ + Оптика очков: делит + 1280×480 на 2 × 640×480 + (L + R) +``` + +### 3.1. Поток данных + +1. Пользователь на телефоне тапает по тачпаду → PhoneApp формирует событие. +2. PhoneApp отправляет событие по WebSocket-каналу в GlassesApp. +3. GlassesApp пересылает событие в VM-сервис через `InputManager.injectInputEvent(...)` — событие попадает в активное приложение (`free.zona`) на VirtualDisplay. +4. `free.zona` рисует кадр в Surface виртуального дисплея. +5. Кадр через `SurfaceTexture` → GL-текстуру → binocular-renderer → 2 копии в `1280×480` физический дисплей очков. +6. Оптика очков раздаёт левую половину на левый глаз, правую — на правый. + +--- + +## 4. Функциональные требования + +### 4.1. Подмодуль `app-glasses-vm` (GlassesApp) + +- Устанавливается на очки (Android 12, API 32, arm64-v8a). +- Запускается в landscape на `displayId=0` (1280×480). +- **UI на Jetpack Compose** (как PhoneApp — единый стек). GL-рендер для видео встроен через `AndroidView { SurfaceView(...) }`. +- Использует binocular-renderer из `:binocular-renderer` для отрисовки кадров VirtualDisplay. +- Поднимает VM-сервис (Shizuku UserService). +- Запускает канал связи (WebSocket-сервер на очках, ждёт подключения от PhoneApp). +- Получает от PhoneApp события управления и отправляет их через `InputManager.injectInputEvent` в активное приложение на VirtualDisplay. + +**Структура Compose-дерева:** + +```kotlin +@Composable +fun GlassesScreen(controller: VmGlassesController, modifier: Modifier = Modifier) { + val state by controller.screenState.collectAsStateWithLifecycle() + Box(modifier = modifier.fillMaxSize().background(Color.Black)) { + // Слой 1: GL рендер кадров VirtualDisplay (для состояния Running) + if (state is ScreenState.Running) { + BinocularRendererView( + frameSource = controller.frameSource, + modifier = Modifier.fillMaxSize(), + ) + } + // Слой 2: Compose overlay (поверх GL или вместо него) + when (state) { + is ScreenState.Starting -> LoadingOverlay("Загрузка...") + is ScreenState.ShizukuMissing -> MessageOverlay("Shizuku не активен. Запустите на телефоне и перезапустите приложение.") + is ScreenState.NoAppSelected -> MessageOverlay("Откройте приложение на телефоне") + is ScreenState.Running -> if (state.overlay == OverlayKind.NoConnection) NoConnectionOverlay() + } + } +} +``` + +**`BinocularRendererView`** — `@Composable` обёртка из rayneo-mercury `:binocular-renderer`: + +```kotlin +@Composable +fun BinocularRendererView( + frameSource: FrameSource, + modifier: Modifier = Modifier, +) { + val renderer = remember(frameSource) { BinocularEglRenderer(frameSource) } + AndroidView( + factory = { ctx -> + SurfaceView(ctx).also { sv -> + sv.holder.addCallback(renderer.createCallback()) + } + }, + modifier = modifier, + ) +} +``` + +Внутри `BinocularEglRenderer` — EGL-стек: создание GL-контекста, шейдеры, обновление текстуры из `SurfaceTexture` (подключенного к Surface виртуального дисплея), отрисовка **в две половины экрана 1280×480** одним drawcall. + +### 4.2. Binocular-renderer (общий модуль в rayneo-mercury) + +- **Публичный API:** `@Composable fun BinocularRendererView(frameSource: FrameSource, modifier: Modifier = Modifier)` — встраивается в любое Compose-дерево через `AndroidView { SurfaceView }`. +- **Внутри:** `BinocularEglRenderer` — класс с EGL-контекстом, шейдерами, текстурой. +- Делит экран **строго по вертикали пополам** одним drawcall: + - Левая половина (x ∈ [0, w/2]) → левый глаз. + - Правая половина (x ∈ [w/2, w]) → правый глаз. +- В обеих половинах рисует **одно и то же изображение** (текущий кадр из источника), без искажений и цветокоррекции. +- Ориентация: фиксированный landscape. +- **Источник кадров** — абстракция (интерфейс `FrameSource`): + - `VideoFrameSource` — для view-mate (берёт кадры из ExoPlayer). + - `SurfaceTextureFrameSource` — для rayneo-vm (берёт кадры из `SurfaceTexture`, подключённого к VirtualDisplay). + +> Это и есть тот самый «один модуль на двоих»: один рендерер, разные источники. GL-стек одинаковый, разница только в том, откуда берутся кадры. + +### 4.3. VirtualDisplay 640×480 + +- Создаётся в **VM-сервисе** через `IDisplayManager.createVirtualDisplay(...)` **без** `VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY`. +- Параметры: + - Размер: 640×480, фиксированно. + - Ориентация: landscape. + - Density: 160 dpi (комфортно для `free.zona`). +- Display виден всей системе, в т.ч. `free.zona` может на нём запуститься. +- Жизненный цикл: создаётся в VM-сервисе по команде из GlassesApp, освобождается при остановке. + +### 4.4. Запуск гостевого приложения + +- В VM-сервисе через `IActivityTaskManager.startActivityAsUser(...)`: + - `ActivityOptions.setLaunchDisplayId(virtualDisplayId)` — целевой display. + - Компонент `free.zona/ru.zona.app.android.MainActivity`. + - `UserHandle.SYSTEM` или `UserHandle.CURRENT` — уточнить на этапе разработки. + +### 4.5. Захват кадров с VirtualDisplay + +- `IDisplayManager.createVirtualDisplay(...)` возвращает **Surface**, в который система рисует кадры `free.zona`. +- Этот Surface пробрасываем из VM-сервиса в GlassesApp (по AIDL-биндеру, как `Parcelable`-обёртку или через `SurfaceTexture`). +- В GlassesApp подключаем к Surface `SurfaceTexture` и через него обновляем GL-текстуру в binocular-renderer. + +### 4.6. Подмодуль `app-phone-vm` (PhoneApp) + +**Общая структура UI** (Compose Navigation): + +``` +HomeScreen +├── таб "Очки" → GlassesStatusScreen (батарея, память, текущее приложение — как в view-mate) +└── таб "Приложения" → AppsListScreen (список AppProfile'ов) + └── тап на приложение → AppControlScreen(profile = ...) (специфичный UI) +``` + +#### 4.6.1. Профили приложений (`AppProfile`) + +У каждого приложения — свой UI управления (тачпад + специфичные кнопки). Общий механизм — интерфейс `AppProfile` в `:app-phone-vm`: + +```kotlin +interface AppProfile { + val packageName: String // "free.zona" + val displayName: String // "Zona" + @Composable + fun Controls( + protocol: VmProtocol, + modifier: Modifier = Modifier, + ) +} + +object ZonaProfile : AppProfile { + override val packageName = "free.zona" + override val displayName = "Zona" + @Composable + override fun Controls(protocol: VmProtocol, modifier: Modifier) { + // тачпад + медиа-кнопки + клавиатура + назад — см. ниже + } +} + +// Будущее: +// object JellyfinProfile : AppProfile { ... } + +val profiles: List = listOf(ZonaProfile /*, JellyfinProfile */) +``` + +#### 4.6.2. ZonaProfile.Controls + +```kotlin +@Composable +override fun Controls(protocol: VmProtocol, modifier: Modifier) { + var showKeyboard by remember { mutableStateOf(false) } + Column(modifier) { + TouchpadView( + onTouch = { actions -> protocol.send(TouchEvent(actions)) }, + modifier = Modifier.weight(1f), // тачпад во всё доступное место + ) + Row(verticalAlignment = Alignment.CenterVertically) { + MediaButton("⏪") { protocol.send(KeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND, DOWN)) } + MediaButton("⏯") { protocol.send(KeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, DOWN)) } + MediaButton("⏩") { protocol.send(KeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, DOWN)) } + MediaButton("⌨") { showKeyboard = true } + MediaButton("←") { protocol.send(KeyEvent(KeyEvent.KEYCODE_BACK, DOWN)) } + } + } + if (showKeyboard) KeyboardDialog(onDismiss = { showKeyboard = false }) { text -> + // Каждый символ → KeyEvent с KEYCODE_* в Zona + for (ch in text) protocol.send(KeyEvent(KeyEvent.keyCodeForChar(ch), DOWN)) + } +} +``` + +**Кнопки:** +- ⏪ `KEYCODE_MEDIA_REWIND` — перемотка назад +- ⏯ `KEYCODE_MEDIA_PLAY_PAUSE` — плей/пауза +- ⏩ `KEYCODE_MEDIA_FAST_FORWARD` — перемотка вперёд +- ⌨ — открывает диалог клавиатуры +- ← `KEYCODE_BACK` — назад + +Все команды отправляются как обычные `KeyEvent` (DTO уже умеет любые `keyCode`). Прибывают на очки → `injectKeyEvent` через VM-сервис в активное приложение. + +#### 4.6.3. Клавиатура — диалог с полем ввода + +Всплывающее окно с `OutlinedTextField` и кнопкой «Отправить»: + +```kotlin +@Composable +fun KeyboardDialog(onDismiss: () -> Unit, onSubmit: (String) -> Unit) { + var text by remember { mutableStateOf("") } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Ввод текста") }, + text = { OutlinedTextField(value = text, onValueChange = { text = it }, singleLine = false) }, + confirmButton = { Button(onClick = { onSubmit(text); onDismiss() }) { Text("Отправить") } }, + dismissButton = { TextButton(onClick = onDismiss) { Text("Отмена") } }, + ) +} +``` + +При «Отправить» каждый символ строки → `KeyEvent` с `keyCode = KeyEvent.keyCodeForChar(ch)` → на очки → `injectKeyEvent`. В Zona текст появляется посимвольно (так работает любая виртуальная клавиатура). + +> **Почему не системная клавиатура:** системная требует управления фокусом Compose + серьёзные проблемы с `WindowInsets`/`ImeAction` в Compose. Поле ввода с кнопкой «Отправить» проще, надёжнее, и UX в данном случае даже удобнее (юзер видит весь ввод перед отправкой). + +#### 4.6.4. Тачпад + +`TouchpadView` — Compose-обёртка над `Box.onTouchEvent`. Маппинг координат: тачпад во весь экран → координаты 0..640 × 0..480 (разрешение VirtualDisplay). Отправляет серии `TouchEvent(actions=[down, move, move, up])`. + +#### 4.6.5. Запуск приложения + +**Запуск GlassesApp на очках** (через Mercury-канал): + +1. PhoneApp обнаруживает, что WebSocket-канал не поднялся (mDNS discovery не нашёл наш сервис). +2. На `GlassesStatusScreen` показывается кнопка «▶ Запустить наше приложение на очках». +3. По нажатию — `MercuryBridge.launchApp("pw.binom.rayneovm.glasses")` через BLE-протокол RayNeo. +4. Очки сами открывают наше приложение (`Intent.PACKAGE_LAUNCH` по пакету). +5. GlassesApp стартует → поднимает WebSocket-сервер → mDNS publish → PhoneApp находит → подключается. + +**Запуск гостевого приложения (Zona)** (через WebSocket-канал): + +1. При тапе на профиль в `AppsListScreen` PhoneApp шлёт `LaunchAppCommand(packageName="free.zona")` через `VmProtocol` (WebSocket). +2. GlassesApp получает → VM-сервис запускает/фокусирует Activity (`IActivityTaskManager.startActivityAsUser` + `setLaunchDisplayId`). +3. Если приложение уже запущено — просто фокусируем. +4. Если не запущено — стартуем. +5. PhoneApp переходит на `AppControlScreen(profile)`. + +#### 4.6.6. Статус очков (таб «Очки») + +**Два независимых канала связи с очками:** + +| Канал | Технология | Назначение | +|---|---|---| +| **Mercury-канал** (служебный) | BLE, фирменный протокол RayNeo Mercury (`pw.binom.mercury:library`) | Спаривание, статус очков (батарея/яркость/WiFi), **запуск нашего GlassesApp по пакету** через `appMarketLaunch` | +| **WebSocket-канал** (основной) | WiFi, наш `:transport` модуль | Управление запущенным GlassesApp: тачи, клавиши, команды Zona | + +Mercury-канал доступен всегда (когда очки спарены), даже если наше GlassesApp ещё не запущено. Используем его для: +1. Проверки, что очки вообще доступны. +2. Получения батареи/статуса. +3. **Запуска GlassesApp одной кнопкой** (`MercuryBridge.launchApp("pw.binom.rayneovm.glasses")` — очки сами открывают наш пакет). +4. Гашения/включения экрана очков, управления WiFi очков, запрета засыпания (heartbeat). + +**Экран `GlassesStatusScreen`:** + +``` +┌──────────────────────────────────────────┐ +│ Очки │ +│ │ +│ Bluetooth-соединение: ✓ подключено │ +│ Батарея очков: 87% │ +│ Яркость экрана: 60% │ +│ WiFi очков: ✓ SSID "HomeNet" │ +│ │ +│ Наше приложение на очках: ✗ не запущено │ ← определяем через mDNS discovery +│ │ +│ [▶ Запустить наше приложение на очках] │ ← кнопка видна если не запущено +│ │ +│ --- WebSocket-канал --- │ +│ Связь с приложением: ✓ / ✗ │ ← через mDNS / WebSocket status +│ Текущее приложение: Zona │ ← из GlassesStatus.currentApp +└──────────────────────────────────────────┘ +``` + +**ViewModel `GlassesStatusViewModel`** держит: +- `mercuryConnected: StateFlow` +- `glassesBattery: StateFlow` +- `glassesBrightness: StateFlow` +- `glassesWifi: StateFlow` +- `appLaunchedOnGlasses: StateFlow` — обновляется через mDNS-сканирование каждые 5с +- `wsConnected: StateFlow` — состояние WebSocket-канала + +#### 4.6.7. UI design choices (зафиксировано) + +| Параметр | Решение | +|---|---| +| UI-фреймворк | **Jetpack Compose + Material 3** | +| Тема | **Только тёмная** (минимум кода, типично для кино-приложения) | +| Навигация | **Bottom navigation bar** (Material 3 `NavigationBar`), табы: «Очки» / «Приложения» | +| Тачпад визуал | Серый/тёмный фон + точка в месте касания (визуальная обратная связь) | +| Архитектура UI | **MVVM**: Composable + ViewModel + StateFlow (через `collectAsStateWithLifecycle`) | + +#### 4.6.8. Shizuku UX + +При запуске GlassesApp проверяем Shizuku: +- **Если Shizuku активен** — работаем штатно. +- **Если Shizuku не активен** — полноэкранный блок с объяснением: + - Что такое Shizuku, зачем он нужен. + - Как установить (`market://...` или ссылка на сайт rikka.app). + - Как активировать в ADB-режиме. + - Кнопка «Открыть настройки Shizuku» (intent на `moe.shizuku.manager`). +- При появлении Shizuku (через listener `Shizuku.addBinderReceivedListener`) — автоматически убираем блок. + +#### 4.6.9. Потеря связи GlassesApp ↔ PhoneApp + +- WebSocket-обрыв → авто-реконнект в фоне с экспоненциальным backoff: 1с → 2с → 4с → 8с → 16с → 30с (макс). +- Параллельно — Snackbar «Связь потеряна, переподключение...». +- При восстановлении — Snackbar «Связь восстановлена». +- При восстановлении — `VmGlassesController` синхронизирует состояние (какое приложение запущено, режим звука и т.п.). + +#### 4.6.10. Архитектура UI (MVVM) + +Каждый экран = Composable + ViewModel. ViewModel держит `StateFlow`, Composable подписывается через `collectAsStateWithLifecycle()`. + +``` +PhoneApp/ +├── ui/ +│ ├── home/ +│ │ ├── HomeScreen.kt # корневой экран с NavigationBar +│ │ ├── HomeViewModel.kt +│ │ └── HomeState.kt +│ ├── glasses_status/ +│ │ ├── GlassesStatusScreen.kt +│ │ ├── GlassesStatusViewModel.kt +│ │ └── GlassesStatusState.kt +│ ├── apps_list/ +│ │ ├── AppsListScreen.kt +│ │ ├── AppsListViewModel.kt +│ │ └── AppsListState.kt +│ ├── app_control/ +│ │ ├── AppControlScreen.kt # обёртка, выбирает AppProfile.Controls +│ │ ├── AppControlViewModel.kt +│ │ └── AppControlState.kt +│ ├── profiles/ +│ │ ├── AppProfile.kt # интерфейс +│ │ └── ZonaProfile.kt +│ ├── components/ +│ │ ├── TouchpadView.kt +│ │ ├── MediaButton.kt +│ │ ├── KeyboardDialog.kt +│ │ └── ConnectionStatus.kt +│ └── theme/ +│ ├── Theme.kt # Material 3, только dark +│ ├── Color.kt +│ └── Type.kt +└── ... +``` + +#### 4.6.11. Обработка краша гостевого приложения + +**Что происходит, когда гостевое приложение (например, `free.zona`) падает:** + +| Что упало | Что остаётся | Что пропадает | +|---|---|---| +| Activity (unhandled exception) | VirtualDisplay, Surface, процесс | Activity мертва, нужно перезапустить | +| Процесс приложения (OOM killer, SIGKILL) | VirtualDisplay, Surface (рисует чёрный/последний кадр) | Процесс мёртв | +| Наш VM-сервис | — | VirtualDisplay + Surface исчезают | +| Наш GlassesApp | VM-сервис живёт в отдельном процессе | WebSocket-сервер падает | + +**Главное:** `VirtualDisplay` привязан к нашему VM-сервису (нашему процессу), а не к процессу гостя. VirtualDisplay остаётся живой после падения гостя — мы просто перезапускаем Activity на ней. + +**Детектирование:** в VM-сервисе подписываемся на `IActivityTaskManager.registerTaskStackListener(...)` (доступно через Shizuku). На каждый `onTaskDestroyed(taskInfo)` проверяем `taskInfo.baseActivity.packageName` — если это гостевое приложение, шлём событие в PhoneApp. + +**Без автоперезапуска** (по решению пользователя): + +1. VM-сервис получает `onTaskDestroyed` для пакета `free.zona`. +2. Шлёт `AppCrashedEvent(packageName)` через WebSocket в PhoneApp. +3. `AppControlViewModel` ловит событие: + - `Snackbar` «Zona упала» + - `navController.popBackStack()` → возврат на `AppsListScreen` +4. Пользователь сам решает — тапает на Zona снова или нет. + +**Без лимита рестартов** (раз нет автоперезапуска, лимит не нужен). + +**DTO:** +```kotlin +@Serializable @SerialName("app_crashed") +data class AppCrashedEvent(val packageName: String) : VmMessage +``` + +**Edge cases (не покрываем на этой ночи, отложено):** +- VM-сервис упал → детектим через `DeadObjectException` на клиенте, перезапуск сервиса. +- GlassesApp упал → WebSocket обрыв → уже покрыто авто-реконнектом + Snackbar (раздел 4.6.9). +- Zona падает в цикле при ручном рестарте → пользователь сам разбирается (видит, что падает). + +#### 4.6.12. Состояния экрана очков (GlassesApp рендерит) + +GlassesApp — это не «прозрачный пайплайн», у него есть **свой overlay-режим** для случаев, когда показывать VirtualDisplay не нужно или он недоступен. Реализуется через `BinocularRenderer` с переключением между двумя `FrameSource`'ами: `VirtualDisplayFrameSource` (основной) и `OverlayFrameSource` (для надписей и заглушек). + +**Состояние 1: «Всё работает»** (VirtualDisplay есть, PhoneApp подключен) +- Источник: `VirtualDisplayFrameSource` +- Что видно: живой кадр из Zona (или любого запущенного приложения). + +**Состояние 2: «Нет связи с PhoneApp»** (VirtualDisplay есть, WebSocket-канал оборван >5с) +- Источник: `OverlayFrameSource` поверх `VirtualDisplayFrameSource` +- Что видно: последний кадр VirtualDisplay + полупрозрачная надпись «⚠ Нет связи» по центру. +- Когда связь восстановилась → возвращаемся к состоянию 1. +- Детектирование: WebSocket `onDisconnected` срабатывает, через 5с показываем overlay. При `onConnected` — убираем. + +**Состояние 3: «Приложение не выбрано»** (VirtualDisplay есть, но гостевое приложение не запущено) +- Источник: `OverlayFrameSource` (полностью перекрывает VirtualDisplay) +- Что видно: чёрный фон + текст по центру «Откройте приложение на телефоне». + +**Состояние 4: «Shizuku не активен»** (GlassesApp стартует, Shizuku не разрешает) +- Источник: `OverlayFrameSource` +- Что видно: чёрный фон + текст «Shizuku не активен. Запустите Shizuku на телефоне и перезапустите приложение». (Телефон отдельно показывает инструкцию в своём UI.) + +**Состояние 5: «Стартует»** (GlassesApp запустился, ещё ничего не инициализировано) +- Источник: `OverlayFrameSource` +- Что видно: чёрный фон + логотип/название «RayNeo VM» + текст «Загрузка...». + +**Управление состоянием:** `VmGlassesController` держит `StateFlow`, рендерер подписывается и переключает `FrameSource` при изменении. + +``` +sealed interface ScreenState { + object Starting : ScreenState + object ShizukuMissing : ScreenState + object NoAppSelected : ScreenState + data class Running(val overlay: OverlayKind = OverlayKind.None) : ScreenState +} + +enum class OverlayKind { None, NoConnection } +``` + +### 4.7. Канал связи и протокол + +#### 4.7.1. Разделение ответственности + +> **Транспорт vs протокол — это разные вещи.** +> - **Транспорт** (модуль `:transport` в `rayneo-mercury`) решает **КАК** передавать байты/строки: WebSocket, BT, реконнект, discovery. Не знает про домен. +> - **Протокол** (в коде rayneo-vm) решает **ЧТО** передавать: DTO, формат (JSON / protobuf / что угодно), семантика событий. + +Это позволяет переиспользовать `:transport` где угодно (другие приложения, другие домены), не копируя код. + +#### 4.7.2. Канал связи (из `:transport`) + +- **WiFi (основной):** WebSocket через Ktor. Телефон поднимает `WifiServerTransport`, очки — `WifiTransport`-клиент. Discovery через `MdnsScanner` (+ fallback скан /24, потому что на RayNeo multicast режется). +- **Bluetooth (fallback):** классический Bluetooth SPP / BLE GATT. `BtServerTransport` на телефоне, `BtTransport` на очках. +- **Автоматика:** авто-реконнект при потере связи, индикация статуса. +- Кто инициирует соединение: **телефон → очки**. Телефон публикует себя (mDNS / BT discoverable), очки находят и подключаются. + +#### 4.7.3. Протокол rayneo-vm (НЕ в либе, в коде приложения) + +Свои DTO, своя сериализация — живут в `app-glasses-vm` / `app-phone-vm`: + +```kotlin +// от Phone → Glasses: тап/свайп по тачпаду +@Serializable data class TouchEvent( + val displayId: Int, + val actions: List // серия down/move/up с координатами 0..640, 0..480 +) + +// от Phone → Glasses: клавиша +@Serializable data class KeyEvent( + val keyCode: Int, + val action: KeyAction // DOWN / UP +) + +// от Glasses → Phone: статус (батарея, текущее приложение, состояние) +@Serializable data class GlassesStatus( + val batteryPercent: Int?, + val currentApp: String?, + val connectionOk: Boolean, + val displayId: Int +) +``` + +Формат — JSON (через kotlinx-serialization). Транспорт `:transport` передаёт их как `String` через `StringTransport.send(...)`. + +### 4.8. Управление / инпут в гостевое приложение + +- PhoneApp → GlassesApp → VM-сервис → `InputManager.injectInputEvent(...)` → активное приложение на VirtualDisplay. +- Координаты тачпада на телефоне мапятся в координаты VirtualDisplay (640×480). +- `injectInputEvent` требует `INJECT_EVENTS` permission — даём через Shizuku UserService (см. 8.4.4). +- Опционально — long-press для контекстного меню, swipe для скролла, multi-touch (pinch-to-zoom, если Zona поддерживает). + +### 4.9. Интеграция с rayneo-mercury + +| Наш модуль | Зависимость от rayneo-mercury | +|---|---| +| `app-glasses-vm` | `:binocular-renderer` (для рендера), `:transport` (для WebSocket-сервера, mDNS-сервера), `:shizuku-vd` (для UserService) | +| `app-phone-vm` | `:transport` (для WebSocket-клиента, mDNS-клиента), `:library` (для Mercury BLE — спаривание, статус, **запуск GlassesApp**) | + +Подключение через Maven-координаты `pw.binom.mercury::`, репо — локальный Nexus `http://192.168.76.117/repository/caffeine/`. Креды — в `~/.gradle/gradle.properties` (уже настроено для существующего `rayneo-mercury:library`). + +--- + +## 4.10. Структура приложения rayneo-vm + +### 4.10.1. Gradle-модули + +``` +rayneo-vm/ +├── settings.gradle.kts # include :app-glasses-vm, :app-phone-vm, :shared +├── build.gradle.kts # root +├── gradle.properties +├── docs/SPEC.md # этот документ +│ +├── shared/ # общий код между двумя приложениями (НЕ в rayneo-mercury) +│ ├── build.gradle.kts # плагин kotlinx-serialization, без зависимостей от rayneo-mercury +│ └── src/commonMain/kotlin/pw/binom/rayneovm/shared/ +│ ├── VmMessage.kt # sealed interface VmMessage — базовый тип +│ ├── TouchEvent.kt # @Serializable @SerialName("touch") data class TouchEvent(...) +│ ├── TouchAction.kt # ACTION_DOWN/MOVE/UP + координаты 0..640, 0..480 +│ ├── KeyEvent.kt # @Serializable @SerialName("key") data class KeyEvent(...) +│ ├── KeyAction.kt # DOWN/UP + keyCode +│ └── GlassesStatus.kt # @Serializable @SerialName("status") data class GlassesStatus(...) +│ +├── app-glasses-vm/ # приложение для очков RayNeo X3 Pro +│ ├── build.gradle.kts # зависимости: :shared, rayneo-mercury:binocular-renderer, +│ │ # rayneo-mercury:transport, rayneo-mercury:shizuku-vd, +│ │ # dev.rikka.shizuku:api, RayNeo X3 Pro SDK +│ └── src/main/... +│ +└── app-phone-vm/ # приложение для телефона + ├── build.gradle.kts # зависимости: :shared, rayneo-mercury:transport, Compose, Activity + └── src/main/... +``` + +> **Важно:** `:shared` — это модуль **внутри** rayneo-vm, не в `rayneo-mercury`. DTO не публикуем, не шарим между проектами. Просто общий код двух приложений одного проекта. + +### 4.10.2. Классы в `app-glasses-vm` + +| Класс | Ответственность | +|---|---| +| `VmGlassesApp : Application` | Инициализация зависимостей, регистрация Shizuku permission listener, запуск `VmService` через `startForegroundService` в `onCreate`. | +| `MainActivity : ComponentActivity` | Compose-host. Ставит fullscreen + immersive, рендерит `GlassesScreen(controller)`. Управляет bindService на `VmService`. | +| `VmService : LifecycleService` | **Foreground service** в процессе GlassesApp. Внутри: WiFi-сервер (WebSocket через `:transport`), mDNS-публикация, `VmProtocol`. Service устойчив к lifecycle Activity — сервер и mDNS живут пока Service foreground. | +| `VmGlassesController` | Оркестратор. Держит ссылки на Service, рендер, протокол. Управляет состоянием экрана (`ScreenState` flow). | +| `VirtualDisplayFrameSource : FrameSource` | Реализация интерфейса из `:binocular-renderer`. Принимает `SurfaceTexture` (получен из VM-сервиса), обновляет GL-текстуру для каждого кадра. | +| `OverlayFrameSource : FrameSource` | Рендерит Compose-overlay (текст, статус) вместо живого видео для состояний `Starting`, `ShizukuMissing`, `NoAppSelected`, `NoConnection`. | +| `VmProtocol` | Обёртка над `StringTransport` (из `:transport`). Декодит входящие строки в `VmMessage`, кодит исходящие. | +| `VmShizukuService` | Клиент к Shizuku UserService. Содержит AIDL-биндер, через который дёргает методы сервиса. | +| `VmShizukuUserService : UserService` | Реализация UserService (запускается в отдельном процессе с правами shell). В нём: создание VirtualDisplay, запуск `free.zona`, проброс Surface обратно. | + +**Lifecycle VmService:** +- `onCreate`: создаёт Ktor `CIO` server engine, регистрирует WebSocket endpoint `/ws/rayneo-vm`, запускает mDNS publisher (`_rayneo-vm._tcp.local`). +- `onStartCommand`: `startForeground(NOTIFICATION_ID, notification)` — держит себя foreground. +- `onDestroy`: останавливает сервер, отменяет mDNS. +- Service не пересоздаётся системой (foreground). + +**Архитектурно:** WebSocket и mDNS живут в **нашем процессе** (GlassesApp), а системные вызовы — в **отдельном процессе** (Shizuku UserService). Связь между ними через AIDL-биндер (тот же `VmShizukuService`). Это даёт: GlassesApp можно убить/перезапустить без потери Shizuku-сессии (но WebSocket перезапустится вместе с ним). + +### 4.10.3. Классы в `app-phone-vm` + +| Класс | Ответственность | +|---|---| +| `VmPhoneApp : Application` | Инициализация зависимостей, инициализация MercuryBridge. | +| `MainActivity : ComponentActivity` | Compose-host. Ставит полноэкранный режим, рендерит `HomeNavGraph`. | +| `VmPhoneController` | Оркестратор. Транспорт (WiFi-клиент через `:transport`), discovery (mDNS/BT scan), протокол, реконнект-логика. Lifecycle. | +| `MercuryBridge` | Обёртка над `pw.binom.mercury:library` для BLE-канала: спаривание, статус очков, **запуск GlassesApp по пакету** через `appMarketLaunch`. | +| `HomeNavGraph` | Compose NavHost. Табы: «Очки» (GlassesStatusScreen) + «Приложения» (AppsListScreen → AppControlScreen). | +| `GlassesStatusScreen : @Composable` | Таб «Очки»: BT-соединение, батарея, статус нашего GlassesApp (через mDNS), кнопка «▶ Запустить наше приложение на очках». | +| `GlassesStatusViewModel` | StateFlow с состоянием Mercury + mDNS discovery + WebSocket. | +| `AppsListScreen : @Composable` | Таб «Приложения»: список профилей (ZonaProfile и т.д.). | +| `AppControlScreen : @Composable` | Обёртка, рендерит `AppProfile.Controls` выбранного профиля. | +| `AppProfile` (interface) | `packageName`, `displayName`, `@Composable Controls(protocol: VmProtocol, modifier)`. | +| `ZonaProfile : AppProfile` | Touchpad + media buttons + keyboard dialog + back. | +| `KeyboardDialog` | Диалог с OutlinedTextField + «Отправить» — каждый символ → `KeyEvent(keyCodeForChar(ch))`. **Не системная клавиатура** (избегаем focus/WindowInsets pain в Compose). | +| `TouchpadController` | Логика тачпада: накапливает тачи, формирует `TouchEvent`, шлёт в `VmProtocol`. | +| `VmProtocol` | Обёртка над `StringTransport`. Кодит/декодит `VmMessage`. | + +### 4.10.3.1. Конкретные DTO `:shared` модуля + +```kotlin +// Базовый sealed interface для всех сообщений +@Serializable +sealed interface VmMessage + +// === PhoneApp → GlassesApp === + +@Serializable @SerialName("touch") +data class TouchEvent(val actions: List) : VmMessage + +@Serializable +data class TouchAction( + val type: TouchType, // DOWN, MOVE, UP + val x: Float, // 0..640 + val y: Float, // 0..480 +) + +@Serializable @SerialName("key") +data class KeyEvent(val keyCode: Int) : VmMessage + +@Serializable @SerialName("launch_app") +data class LaunchAppCommand(val packageName: String) : VmMessage + +@Serializable @SerialName("set_audio_mode") +data class SetAudioMode(val mode: AudioMode) : VmMessage // ON_GLASSES, ON_PHONE + +@Serializable @SerialName("shutdown") +object Shutdown : VmMessage // закрыть гостевое приложение + +// === GlassesApp → PhoneApp === + +@Serializable @SerialName("glasses_status") +data class GlassesStatus( + val batteryPercent: Int? = null, + val wifiSsid: String? = null, + val currentApp: String? = null, // package name или null + val running: Boolean = false, +) : VmMessage + +@Serializable @SerialName("app_crashed") +data class AppCrashedEvent(val packageName: String) : VmMessage + +@Serializable @SerialName("app_launched_ack") +data class AppLaunchedAck(val packageName: String, val success: Boolean) : VmMessage + +// === Общие === + +@Serializable @SerialName("ping") +object Ping : VmMessage + +@Serializable @SerialName("pong") +object Pong : VmMessage + +// === Enums === + +@Serializable +enum class TouchType { DOWN, MOVE, UP } + +@Serializable +enum class AudioMode { ON_GLASSES, ON_PHONE } +``` + +### 4.10.3.2. Тестовая стратегия + +**Фреймворки:** JUnit5 (`org.junit.jupiter`) + MockK + Turbine + kotlinx-coroutines-test. + +**Что покрываем unit-тестами (JVM, без Android):** +- `VmProtocol` — roundtrip encode/decode всех DTO. +- `TouchpadController` — маппинг touch-coordinates, батчинг событий. +- `KeyboardDialog`-логика — перевод строки в keyCode (`KeyEvent.keyCodeForChar('A') = KEYCODE_A`). +- `VmGlassesController` (через `runTest`/`TestDispatcher`) — переходы между `ScreenState`. +- `VmPhoneController` — reconnect backoff логика (1с→2с→4с→8с→16с→30с). +- `TouchAction`/`TouchType` enum сериализация. +- `AudioMode`/`OverlayKind` enum сериализация. +- `LaunchAppCommand` формирование. +- `GlassesStatus`/`AppCrashedEvent` сериализация. + +**Что НЕ покрываем unit-тестами (требует устройства):** +- GL рендер (бинокль). +- VirtualDisplay creation через Shizuku. +- WebSocket транспорт (нужен Ktor runtime). +- mDNS discovery. +- Mercury BLE. +- Activity injection events. + +### 4.10.4. Поток данных + +``` +[Zona на VirtualDisplay] [Тефон] + │ │ + │ кадры в Surface │ + ▼ │ +[VM-сервис: создал display, запустил Zona] │ + │ │ + │ Surface виртуального дисплея │ + ▼ │ +[VmGlassesController] │ + │ │ + │ SurfaceTexture подключен │ + ▼ │ +[VirtualDisplayFrameSource] ──► GL-текстура ──► BinocularRenderer │ + (2 копии в 1280×480) │ + │ │ + ▼ ▼ + [Очки: оптика делит на 2 глаза] + │ + Touch/Key events ────────┘ + │ + WifiTransport.bt() / .wifi() + │ + ▼ + [VmProtocol → UserService → injectInputEvent → Zona] +``` + +### 4.10.5. Звук — два режима + +> **Важно:** к RayNeo X3 Pro **нельзя подключить наушники** (ни BT, ни USB) — конструктивное ограничение очков. Встроенный динамик очков тихий и низкокачественный. Поэтому передача звука **на телефон** (где у пользователя могут быть нормальные наушники) — **основной use case**, а не опция. +> +> Переключатель режима — на телефоне (UI в `TouchpadScreen`), сохраняется в настройках, синхронизируется с очками через `AudioModeCommand`. + +#### Режим 1: звук на очках (встроенный динамик) + +- Сценарий: отладка, тихое проигрывание «для проверки», просмотр без наушников. +- **Подтверждено:** `VirtualDisplay` в Android — **только видео** (документация: `getSurface`/`getDisplay`/etc., ни одного аудио-API). Аудио идёт отдельным каналом через системный `AudioFlinger` в output device. У RayNeo X3 Pro есть встроенный динамик (`STREAM_MUSIC Devices: speaker`). +- `free.zona` воспроизводит звук штатно через свой `AudioTrack` / `MediaPlayer` в `USAGE_MEDIA/CONTENT_TYPE_MOVIE`. Системный микшер очков играет его во встроенный динамик. +- **Наш код не вмешивается.** Работает из коробки. + +#### Режим 2: звук передаётся на телефон (основной сценарий) + +- Сценарий: пользователь смотрит фильм/контент на очках, слушает через наушники, подключённые к **телефону** (BT-гарнитура, USB-C наушники, встроенный динамик телефона). +- Передача аудио — ключевая фича. Без неё приложение малополезно для контента со звуком. + +Цепочка: + +``` +[free.zona на очках] + │ AudioTrack → системный микшер + ▼ +[AudioPlaybackCapture наш] ← AudioRecord с config USAGE_MEDIA + │ PCM-сэмплы + ▼ +[Кодек на очках] ← PCM напрямую или Opus + │ байты + ▼ +[ByteTransport на очках] ← Transport.wifi(url).send(bytes) или .bt(...) + │ + │ WiFi / BT + ▼ +[ByteTransport на телефоне] ← принимаем поток + │ PCM + ▼ +[AudioTrack на телефоне] ← воспроизведение в динамик/наушники + ▼ +[Пользователь слышит звук с телефона] +``` + +Параллельно — на очках **приглушаем** `STREAM_MUSIC` через `AudioManager`, чтобы не было дубля (звук играет и на очках, и на телефоне). + +**Ключевые компоненты:** +- `AudioCaptureService` (в `:app-glasses-vm`) — обёртка над `AudioPlaybackCapture` + `AudioRecord`. Захват PCM с нужной конфигурацией. +- `AudioStreamer` (в `:app-glasses-vm`) — читает из `AudioCaptureService`, кодит (PCM/Opus), шлёт через `ByteTransport`. +- `AudioPlayer` (в `:app-phone-vm`) — принимает PCM, играет через `AudioTrack`. + +**Технические нюансы:** + +| Нюанс | Решение | +|---|---| +| `free.zona` может запретить захват (ALLOW_CAPTURE_BY_SYSTEM вместо _BY_ALL) | При невозможности захвата — переключаемся в режим 1, сообщаем пользователю «приложение не разрешает захват звука». | +| Задержка передачи | WiFi ~30-100 мс (норм), BT ~100-300 мс (ощутимо). Буферизация + визуальный индикатор задержки. | +| Формат передачи | **PCM 44.1 kHz 16 bit stereo** через WiFi (176 KB/s, помещается). Через BT — Opus ~30 KB/s (TBD). | +| **Проигрывание звука на телефоне в фоне** | **Foreground service `AudioPlaybackService`** типа `mediaPlayback` с `PARTIAL_WAKE_LOCK` + `WIFI_MODE_FULL_HIGH_PERF` (см. ниже). | +| **Захват звука на очках в фоне** | **Foreground service `AudioCaptureService`** типа `mediaProjection` для удержания захвата. | + +#### 4.10.5.1. Background playback — foreground service + +**Критичная проблема:** начиная с Android 8.0 (API 26) фоновая работа ограничена. **Китайские прошивки** (HONOR, MIUI, EMUI, ColorOS, OriginOS) **агрессивно убивают фоновые процессы** при выключенном экране. View-mate подтверждает: «HONOR замораживает/убивает процесс в фоне → очки теряют связь (видео паузится) и звук встаёт». + +**Решение (по образцу `view-mate/app-phone/PlaybackService.kt`):** + +```kotlin +// app-phone-vm/.../audio/AudioPlaybackService.kt +class AudioPlaybackService : Service() { + private var wakeLock: PowerManager.WakeLock? = null + private var wifiLock: WifiManager.WifiLock? = null + + override fun onCreate() { + super.onCreate() + // 1. Foreground notification (ОБЯЗАТЕЛЬНО для Android 8+) + startForeground(NOTIFICATION_ID, buildNotification()) + // 2. Wake lock — CPU не засыпает при выключенном экране + wakeLock = powerManager.newWakeLock(PARTIAL_WAKE_LOCK, "rayneo-vm:audio")?.apply { acquire() } + // 3. WiFi lock — WiFi не отключается (иначе WebSocket-канал падает) + wifiLock = wifiManager.createWifiLock(WIFI_MODE_FULL_HIGH_PERF, "rayneo-vm:wifi")?.apply { acquire() } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = START_STICKY + override fun onDestroy() { /* release locks */ super.onDestroy() } + override fun onBind(intent: Intent?): IBinder? = null +} +``` + +**Lifecycle:** +- Стартует когда приходит `SetAudioMode(ON_PHONE)` от пользователя. +- Останавливается когда приходит `SetAudioMode(ON_GLASSES)`. +- НЕ всегда-on — только когда нужен режим 2. + +**Аналогичный `AudioCaptureService` на очках** (для удержания `AudioPlaybackCapture`): +- Стартует на очках в `VmService.onCreate` когда активен режим 2. +- Держит `AudioRecord` живым. +- Foreground service типа `mediaProjection` (Android 14+ требует `FOREGROUND_SERVICE_MEDIA_PROJECTION`). + +**Permissions для foreground service (Android 14+, API 34):** + +| Permission | Кто | Зачем | +|---|---|---| +| `FOREGROUND_SERVICE` | Оба приложения | Базовое разрешение для foreground service. | +| `FOREGROUND_SERVICE_MEDIA_PLAYBACK` | PhoneApp | Тип `mediaPlayback` для AudioPlaybackService. | +| `FOREGROUND_SERVICE_MEDIA_PROJECTION` | GlassesApp | Тип `mediaProjection` для AudioCaptureService. | +| `WAKE_LOCK` | PhoneApp | Для `PARTIAL_WAKE_LOCK`. | +| `POST_NOTIFICATIONS` | Оба (Android 13+) | Чтобы foreground notification показывалась. | + +**Важно:** начиная с Android 14 (`targetSdk >= 34`) **каждый foreground service тип должен быть явно объявлен в манифесте** через ``. +| Синхронизация A/V | Звук на телефоне может рассинхронизироваться с картинкой в очках. Решается через таймстампы в PCM-пакетах + буфер на принимающей стороне. | + +--- + +## 5. Нефункциональные требования + +| Параметр | Значение | +|---|---| +| Целевой FPS | ≥ 30, желательно 60 | +| Суммарная задержка «тап на телефоне → реакция в Zona» | < 100 мс | +| Суммарная задержка «кадр VirtualDisplay → очки» | < 1 кадра | +| Энергопотребление | без явных требований, но без лишнего копирования CPU↔GPU | +| Размер виртуального экрана | строго 640×480 | +| Минимальный Android API (GlassesApp) | 32 (Android 12) — соответствует RayNeo X3 Pro | +| Минимальный Android API (PhoneApp) | 26 (Android 8.0) — для совместимости с типичными телефонами | +| Целевое устройство GlassesApp | RayNeo X3 Pro (ARGF20) | +| Версия Kotlin | 1.9.x | +| Compose BOM | 2024.x | +| AGP | 8.x | + +### 5.1. Сетевые настройки + +| Параметр | Значение | +|---|---| +| WebSocket-порт на очках | **8080** (WebSocket + путь `/ws/rayneo-vm`) | +| mDNS service type | **`_rayneo-vm._tcp`** | +| TLS | Нет (локальная сеть, устройства пользователя) | +| Аутентификация | Нет (только локальная сеть) | + +### 5.2. Имена пакетов и namespace + +| Компонент | Namespace | +|---|---| +| GlassesApp | `pw.binom.rayneovm.app.glasses` | +| PhoneApp | `pw.binom.rayneovm.app.phone` | +| `:shared` | `pw.binom.rayneovm.shared` | +| rayneo-mercury новые модули | `pw.binom.mercury.transport`, `pw.binom.mercury.binocularrenderer`, `pw.binom.mercury.shizukuvd` | + +### 5.3. Имя приложения (для пользователя) + +- **GlassesApp:** «RayNeo VM» (лаунчер иконка) +- **PhoneApp:** «RayNeo VM» (лаунчер иконка) + +### 5.3. Permissions + +**GlassesApp (`app-glasses-vm/AndroidManifest.xml`):** + +```xml + + + + + + + + + + +``` + +**PhoneApp (`app-phone-vm/AndroidManifest.xml`):** + +```xml + + + + + + + + + + + + + +``` + +### 5.4. Дефолты (фиксировано для ночной сессии) + +**Версии — latest stable на сентябрь 2026 (по правилу «брать последнее по возможности»):** + +| Что | Версия | Проверено | +|---|---|---| +| Kotlin | **2.4.20** | Maven Central | +| AGP (Android Gradle Plugin) | **9.4.1** | Maven Google | +| Gradle | **9.x** (совместимая с AGP 9.4.1) | Maven Google | +| Compose BOM | **2026.09.00** | Maven Google | +| AndroidX Lifecycle (вкл. `lifecycle-runtime-compose`) | **2.11.0** | Maven Google | +| AndroidX Navigation Compose | **2.10.2** | Maven Google | +| Ktor (server + client) | **3.6.0** | Maven Central | +| kotlinx-coroutines | **1.11.0** | Maven Central | +| kotlinx-serialization | **1.11.0** | Maven Central | +| Shizuku API (`dev.rikka.shizuku:api`) | **13.1.5** | Maven Central | +| rayneo-mercury | `0.1.0-SNAPSHOT` (текущая в Nexus) | локально | + +**Заметки:** +- Compose Compiler **встроен в Kotlin plugin** (с Kotlin 2.0+), не нужна отдельная `kotlinCompilerExtensionVersion`. +- compileSdk для PhoneApp: **36** (Android 16), minSdk: **26** (Android 8.0). +- compileSdk для GlassesApp: **36** (для совместимости с актуальными либами), но фактически работаем на API 32. +- Подпись APK: **debug** (для отладки и ночного тестирования), release потом. +- Иконка приложения: стандартная Android (потом кастомная). + +--- + +## 6. Технические ограничения и риски + +- **Зависимость от Shizuku** — без активного Shizuku GlassesApp не работает. Нужна обработка: показать «запустите Shizuku», предложить инструкцию. +- **Зависимость от связи** — без PhoneApp очки работают только в пассивном режиме (можно смотреть, но не управлять). +- **SurfaceTexture ↔ VirtualDisplay** — в некоторых версиях Android нестабильно. Держать в голове план Б через `ImageReader`. +- **Права shell (UID 2000)** в Shizuku ADB-режиме могут быть недостаточны для части операций. Если упрёмся — план Б: Sui (UID 0). +- **multicast режется на RayNeo** — fallback на скан /24 (как в view-mate). +- **RayNeo X3 Pro vs X2** — view-mate писался под X2. Архитектура та же, но возможны мелкие отличия (дисплей 1280×480 vs другой размер). Тестируем на X3 Pro. + +--- + +## 7. План разработки + +### Объём первой ночи + +**Делаем всё, включая коммуникацию между устройствами. Утром — отладка.** + +Конкретно: +1. Все модули `rayneo-mercury`: `:transport`, `:binocular-renderer`, `:shizuku-vd` (publish в Nexus). +2. Все модули `rayneo-vm`: `:shared`, `:app-glasses-vm`, `:app-phone-vm`. +3. Unit-тесты для всей бизнес-логики. +4. Полная коммуникация: телефон находит очки по mDNS → WebSocket → UI управления Zona на телефоне → тач/клавиши → на очках → через VM-сервис → `injectInputEvent` → в Zona. + +### Этапы (последовательность) + +#### Этап 0. Подготовка `rayneo-mercury` (~2ч) + +1. `:transport` — перенести код из view-mate/lib-core, namespace `pw.binom.mercury.transport`, API `Transport`/`ServerTransport` + DSL `Transport.wifi(...)`/`.bt(...)`/`.wifiServer(...)`/`.btServer()`. Зависимости: Ktor + kotlinx-serialization. Unit-тесты для DSL и контрактов. +2. `:binocular-renderer` — обобщить `BinocularVideoPlayer.kt`, интерфейс `FrameSource` + реализации `VideoFrameSource` (для view-mate, **но реализацию делаем минимальной — заглушка, она view-mate нужна только для совместимости**) и `SurfaceTextureFrameSource` (для rayneo-vm, основная). +3. `:shizuku-vd` — AIDL-биндер + UserService обёртка (`createVirtualDisplay`, `startActivityAsUser`, `injectInputEvent`). +4. Опубликовать все три в локальный Nexus. + +#### Этап 1. Bootstrap `rayneo-vm` (~1ч) + +5. Gradle-проект rayneo-vm с тремя модулями (`:shared`, `:app-glasses-vm`, `:app-phone-vm`). +6. `gradle.properties`, `settings.gradle.kts`, корневой `build.gradle.kts` (плагины: AGP, Kotlin, Compose, kotlinx-serialization). +7. Подключить зависимости из Nexus + Shizuku API + RayNeo SDK. +8. Минимальные `MainActivity` в обоих приложениях + build OK. + +#### Этап 2. Модуль `:shared` (~30мин) + +9. DTO: `VmMessage` (sealed), `TouchEvent`, `TouchAction`, `KeyEvent`, `KeyAction`, `GlassesStatus`, `LaunchAppCommand`, `AudioModeCommand`. +10. Сериализация (kotlinx-serialization, JSON). +11. Unit-тесты: roundtrip сериализации, парсинг всех вариантов `VmMessage`. + +#### Этап 3. `:app-glasses-vm` — рендер (~1ч) + +12. `VmGlassesApp : Application` (инициализация Shizuku listener). +13. `MainActivity` (fullscreen + immersive). +14. `VmGlassesController` — оркестратор. +15. `VirtualDisplayFrameSource : FrameSource` — пока с `SurfaceTexture` от чёрного bitmap (заглушка). +16. `BinocularRenderer` — рендерит в обе половины. +17. Unit-тесты для `FrameSource` контракта. + +#### Этап 4. `:app-glasses-vm` — VM-сервис и VirtualDisplay (~2ч) + +18. AIDL: `IVmShizukuService` с методами `createVirtualDisplay()` / `launchApp(packageName)` / `injectInputEvent(event)`. +19. `VmShizukuUserService : UserService` — реализация в отдельном процессе с правами shell: + - `IDisplayManager.createVirtualDisplay(...)` без `OWN_CONTENT_ONLY`. + - `IActivityTaskManager.startActivityAsUser(...)` для `free.zona`. + - `InputManager.injectInputEvent(...)` для инпута. +20. `VmShizukuService` (клиент) — биндинг через `Shizuku.bindUserService`. +21. Surface виртуального дисплея → через биндер в `MainActivity` → `VirtualDisplayFrameSource`. + +#### Этап 5. `:app-phone-vm` — UI (~2ч) + +22. Material 3 тёмная тема. +23. `HomeScreen` + `HomeViewModel` + bottom navigation. +24. `GlassesStatusScreen` + ViewModel. +25. `AppsListScreen` + ViewModel + `AppProfile`. +26. `AppControlScreen` + ViewModel. +27. `ZonaProfile` (тачпад + медиа-кнопки + клавиатура + назад). +28. `TouchpadView`, `MediaButton`, `KeyboardDialog`, `ConnectionStatus`. +29. `VmPhoneController` (транспорт, протокол, lifecycle). +30. Unit-тесты для ViewModel'ей и `VmProtocol`. + +#### Этап 6. Коммуникация (~2ч) + +31. В GlassesApp: `WifiServerTransport` (поднимаем WebSocket-сервер на очках). +32. mDNS discovery на телефоне (через `:transport` `MdnsScanner`). +33. В PhoneApp: `WifiTransport`-клиент, подключение по найденному URL. +34. `VmProtocol` — обёртка, кодит/декодит VmMessage. +35. Передача `TouchEvent`/`KeyEvent` Phone → Glasses → VM-сервис → `injectInputEvent` в Zona. +36. Передача `LaunchAppCommand`. +37. Авто-реконнект с экспоненциальным backoff + Snackbar. +38. Unit-тесты: маппинг координат, формирование KeyEvent из символов, парсинг входящих сообщений. + +#### Этап 7. Звук — режим 2 (~2ч, если успеем) + +39. `AudioCaptureService` (AudioPlaybackCapture, USAGE_MEDIA). +40. `AudioStreamer` (PCM → ByteTransport). +41. `AudioPlayer` (ByteTransport → AudioTrack на телефоне). +42. `AudioModeCommand` в протоколе. +43. Приглушение звука на очках в режиме 2. + +#### Этап 8. Shizuku UX (~30мин) + +44. `ShizukuStatusChecker` — проверка при старте. +45. `ShizukuActivationScreen` — экран с инструкцией и кнопкой. +46. Listener на `Shizuku.addBinderReceivedListener` для авто-скрытия блока. + +### Что НЕ делаем на этой ночи + +- Аналитику, крэш-репорты. +- Логирование в файл (только logcat). +- Поддержку нескольких одновременных подключений к GlassesApp. +- Сложные жесты (мультитач, pinch-to-zoom). +- A/B-тесты, дизайн-систему вне Material 3. +- CI/CD. +- Авто-установку приложений, которых нет на очках. + +### Что проверяем утром + +- Видна ли Zona в очках. +- Работает ли тачпад (тап/свайг). +- Работают ли медиа-кнопки (⏪⏯⏩). +- Работает ли клавиатура (ввести название фильма, найти). +- Работает ли кнопка назад. +- Работает ли авто-реконнект (выключить WiFi на очках, включить — должно восстановиться). +- Работает ли режим 2 звука (если сделали). + +--- + +## 8. Целевое устройство, гостевое приложение и привилегии + +### 8.1. Устройство (подтверждено через `adb`) + +| Параметр | Значение | +|---|---| +| Manufacturer | RayNeo | +| Модель | ARGF20 | +| Android | 12 | +| API level | 32 | +| ABI | arm64-v8a | +| Физический дисплей | **1 шт**, 1280×480, 60 FPS, density 160 dpi | +| `DisplayInfo.flag` | `FLAG_SECURE`, `FLAG_SUPPORTS_PROTECTED_BUFFERS` | + +### 8.2. Дисплей очков + +RayNeo X3 Pro система видит как **один** дисплей `1280×480`. Оптика очков сама делит картинку на две половины по 640×480 и подаёт каждую на свой глаз. + +- Никаких «двух display id» (L / R) нет — есть один `displayId=0`. +- GlassesApp просто рисует картинку 1280×480, где **левая половина → левый глаз, правая → правый**. +- VirtualDisplay 640×480 по размеру = одной «глазной» половине. Удобно. + +### 8.3. Гостевое приложение + +**Zona** (`free.zona`) — онлайн-кинотеатр. + +| Параметр | Значение | +|---|---| +| Package | `free.zona` | +| `MainActivity` | `free.zona/ru.zona.app.android.MainActivity` | +| `versionName` / `versionCode` | `3.0.61` / `1061` | +| `targetSdk` / `minSdk` | `29` / `21` | +| Размер APK | ~19 MB | +| Intent-filter | `android.intent.action.MAIN` ✓ | + +### 8.4. Shizuku + +Установлен и активирован **Shizuku Manager** (пакет `moe.shizuku.privileged.api`, версия `13.6.0`, ADB-режим, `shizuku_server` от UID 2000). + +Через Shizuku делаем всю работу с системными API: +1. Создание VirtualDisplay **без** `VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY`. +2. Запуск **чужой** Activity на этом дисплее через `IActivityTaskManager.startActivityAsUser`. +3. Передача инпута в гостевое приложение через `InputManager.injectInputEvent`. + +#### 8.4.1. Используемые API Shizuku + +- **UserService** (`Shizuku.bindUserService`) — наш VM-сервис в отдельном процессе с правами shell. +- **Permission** — `Shizuku.requestPermission(REQUEST_CODE)`. +- **Provider** — `rikka.shizuku.ShizukuProvider` в `AndroidManifest.xml`. + +API Shizuku: `dev.rikka.shizuku:api:13.1.5+`. + +#### 8.4.2. Разрешения в `AndroidManifest.xml` + +```xml + + + + + + +``` + +#### 8.4.3. Альтернативы Shizuku (резюме) + +Подробный разбор в v0.5 ТЗ (см. историю изменений). Короткий вывод: + +| Способ | Подходит? | +|---|---| +| **Shizuku (UID 2000) через UserService** | ✓ основной | +| **Root / Sui (UID 0)** | ✓ план Б, требует Magisk | +| `DisplayManager.createVirtualDisplay()` без Shizuku | ✗ (только OWN_CONTENT_ONLY) | +| `MediaProjection` | ✗ (только захват) | +| `Presentation` | ✗ (только своё UI) | +| Подпись ключом RayNeo | ✗ (недостижимо) | + +--- + +## 9. Общая либа `rayneo-mercury` — новые модули + +### 9.1. Репозиторий + +`/home/subochev/common-projects/WORK/rayneo-mercury/` — Android Library, публикуется в локальный Nexus `http://192.168.76.117/repository/caffeine/` под groupId `pw.binom.mercury`. + +### 9.2. Новые модули (добавляем) + +#### `:transport` + +- Назначение: **универсальный** канал связи клиент↔сервер по сети. Не привязан к домену RayNeo, очкам или приложению. +- Источник кода: `view-mate/lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/net/` (`GlassesTransport.kt`, `WifiGlassesTransport.kt`, `GlassesWsClient.kt`, `BtTransportConstants.kt`), `view-mate/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/` (`MdnsClient.kt`, `PhoneScan.kt`). +- **Принцип разделения:** либа решает только **КАК** передавать байты/строки (транспорт, реконнект, discovery). **ЧТО** передавать — решает вызывающий (DTO и формат сериализации — в коде приложения, не в либе). +- Namespace: `pw.binom.mercury.transport`. Зависимости — Ktor + kotlinx-serialization. Без KMP (Android Library, как и `library` в rayneo-mercury). + +**Публичный API:** + +```kotlin +// Клиентский транспорт: подключается к серверу, отправляет/получает сообщения +interface Transport { + val name: String + suspend fun connect( + onMessage: suspend (T) -> Unit, + onConnected: suspend () -> Unit, + onDisconnected: suspend () -> Unit, + ) + suspend fun send(message: T) + fun close() +} + +// Серверный транспорт: принимает подключения, шлёт по connId +interface ServerTransport { + val name: String + suspend fun accept( + onMessage: suspend (connId: String, message: T) -> Unit, + onConnected: (connId: String) -> Unit, + onDisconnected: (connId: String) -> Unit, + ) + suspend fun send(connId: String, message: T) + fun close() +} + +// Удобные typealias'ы для типичных кейсов +typealias StringTransport = Transport // JSON-строки, текст +typealias ByteTransport = Transport // бинарный (protobuf, msgpack, raw) +``` + +**DSL-фабрики** (extension functions на `Transport.Companion` — короткий идиоматичный вызов): + +```kotlin +Transport.wifi("ws://192.168.1.42:8080/ws") // StringTransport, клиент +Transport.wifiServer(port = 8080) // ServerTransport, сервер +Transport.bt("00:11:22:33:44:55") // StringTransport, клиент +Transport.btServer() // ServerTransport, сервер +``` + +Это просто синтаксический сахар над конструкторами конкретных классов — никакой магии. + +**Реализации:** + +| Класс | Роль | Транспорт | +|---|---|---| +| `WifiTransport(url: String)` | клиент | WebSocket через Ktor | +| `WifiServerTransport(port: Int)` | сервер | WebSocket через Ktor | +| `BtTransport(macAddress: String)` | клиент | Bluetooth SPP / BLE | +| `BtServerTransport` | сервер | Bluetooth SPP / BLE | + +**Discovery (отдельные классы, не часть `Transport`):** + +| Класс | Что делает | +|---|---| +| `MdnsScanner(serviceType: String, fallbackScan: Boolean = true)` | mDNS discovery + fallback на скан /24 (на некоторых устройствах multicast режется) | +| `BtScanner` | Bluetooth discovery | + +**Чего в либе НЕТ** (намеренно): +- Никаких DTO с `@SerialName("touch")` / `GlassesToHost` / `HostToGlasses`. +- Никакого знания про режимы (`MOVIE`, `CHAT`) или жесты. +- Никаких упоминаний view-mate, RayNeo или конкретного приложения в именах/комментариях. + +**Использование в rayneo-vm (в коде приложения, не в либе):** + +```kotlin +// Свои DTO и протокол — на стороне приложения +@Serializable sealed interface VmMessage +@Serializable @SerialName("touch") data class TouchEvent(...) : VmMessage +@Serializable @SerialName("key") data class KeyEvent(...) : VmMessage +@Serializable @SerialName("status") data class GlassesStatus(...) : VmMessage + +// Свой адаптер поверх транспорта +class VmProtocol(transport: StringTransport) { + val incoming: Flow = callbackFlow { + transport.connect( + onMessage = { json -> trySend(Json.decodeFromString(json)) }, + onConnected = { /* update UI */ }, + onDisconnected = { /* update UI */ }, + ) + awaitClose { transport.close() } + } + suspend fun send(msg: VmMessage) = transport.send(Json.encodeToString(msg)) +} +``` + +- Maven-координаты: `pw.binom.mercury:transport:`. + +#### `:binocular-renderer` + +- Назначение: EGL-бинокулярный рендерер на очках. +- Источник кода: `view-mate/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/ui/BinocularVideoPlayer.kt`. +- Адаптация: выделить интерфейс `FrameSource`. Реализации: + - `VideoFrameSource` (для view-mate, ExoPlayer → Surface → текстура). + - `SurfaceTextureFrameSource` (для rayneo-vm, VirtualDisplay surface → SurfaceTexture → текстура). +- Maven-координаты: `pw.binom.mercury:binocular-renderer:`. + +#### `:shizuku-vd` (опционально) + +- Назначение: удобная обёртка над Shizuku UserService для создания VirtualDisplay и запуска Activity. +- Реализация: биндинг между основным приложением и UserService через AIDL. +- В VM-сервисе: вся логика создания display + запуска Activity + проброс Surface. +- Maven-координаты: `pw.binom.mercury:shizuku-vd:`. + +### 9.3. Версионирование и публикация + +- Версия модулей берётся из `gradle.properties` (`mercuryVersion`, по аналогии с существующим `library`). +- Публикация: `./gradlew :transport:publish :binocular-renderer:publish` → Nexus `caffeine`. +- Подключение в rayneo-vm: + ```kotlin + implementation("pw.binom.mercury:transport:$mercuryVersion") + implementation("pw.binom.mercury:binocular-renderer:$mercuryVersion") + implementation("pw.binom.mercury:shizuku-vd:$mercuryVersion") + ``` + +### 9.4. Что делаем с view-mate + +**Ничего.** view-mate продолжает жить со своим lib-core. Миграция на rayneo-mercury — задача другого агента (по просьбе пользователя). + +--- + +## 10. Открытые вопросы + +### Закрытые (решены) + +- [x] Гостевое приложение — **Zona (`free.zona`)**, см. 8.3. +- [x] Целевое устройство — RayNeo X3 Pro (ARGF20, Android 12, API 32), см. 8.1. +- [x] Display id очков — **один** (`displayId=0`, 1280×480), оптика делит на два глаза по 640×480, см. 8.2. +- [x] Привилегии — через **Shizuku**, см. 8.4. +- [x] Альтернативы Shizuku — единственный практический путь, см. 8.4.3. +- [x] Связь телефон ↔ очки — WebSocket по WiFi + BT-fallback через модуль `:transport`, см. 4.7. +- [x] Общая либа — rayneo-mercury (новые модули `:transport`, `:binocular-renderer`, `:shizuku-vd`), см. 9. +- [x] Протокол — расширяем существующий из view-mate (TouchEvent, KeyEvent, GlassesStatus), см. 4.7.2. +- [x] Звук: 2 режима (на очках / на телефоне), переключатель в PhoneApp, см. 4.10.5. +- [x] UI PhoneApp: Material 3, тёмная тема, bottom navigation, тачпад с серым фоном и точкой касания. +- [x] Shizuku UX: блок + кнопка активации + инструкция. +- [x] Потеря связи: авто-реконнект с экспоненциальным backoff + Snackbar. +- [x] Архитектура UI: MVVM + ViewModel + StateFlow. +- [x] Тесты: unit на JVM для всего что можно. + +### Открытые (проверим экспериментально утром) + +- [ ] Подтвердить экспериментально, что через Shizuku UserService создаётся VirtualDisplay **без** OWN_CONTENT_ONLY и на нём реально стартует `free.zona`. +- [ ] Режим 2 звука: подтвердить, что `AudioPlaybackCapture` работает на `free.zona` (не запрещает ли она захват). Если запрещает — fallback на режим 1 + сообщение. +- [ ] Режим 2 звука: PCM напрямую или сжимать (Opus)? PCM проще, но ~176 KB/s; Opus ~30 KB/s, но добавляет кодек. Уточнить после замера задержки. +- [ ] Режим 2 звука: использовать ли BT-канал для передачи звука (медленнее) или только WiFi (быстрее)? +- [ ] Какие требования по задержке и FPS — есть ли порог «некомфортно»? + +### Отложенные (будущее, не сейчас) + +- [ ] Нужно ли давать пользователю UI-управление на самих очках (жесты/кнопки), или только с телефона? +- [ ] Нужен ли multi-touch (pinch-to-zoom) или достаточно single-touch? +- [ ] Поддержка других приложений (Jellyfin и т.п.) — добавлять новые `AppProfile`. +- [ ] Подтвердить, что RayNeo X3 Pro ведёт себя так же, как X2 (на котором написан view-mate), по части дисплея, жестов, multicast и т.д. + +--- + +## 11. История изменений + +| Дата | Версия | Что изменилось | +|---|---|---| +| 2026-09-25 | 0.1 | Первичный черновик | +| 2026-09-25 | 0.2 | Уточнены: целевое устройство, дисплей, гостевое приложение = Zona. | +| 2026-09-25 | 0.3 | Добавлен раздел про Shizuku. | +| 2026-09-25 | 0.4 | Архитектура обновлена под Shizuku UserService. | +| 2026-09-25 | 0.5 | Раздел про альтернативы Shizuku, отложенный инпут. | +| 2026-09-25 | 0.6 | Архитектура расширена: два подмодуля (GlassesApp + PhoneApp) + общая либа `rayneo-mercury` с новыми модулями `:transport`, `:binocular-renderer`, `:shizuku-vd`. Связь через WebSocket, протокол расширен TouchEvent/KeyEvent/GlassesStatus. Источник переиспользования — `/home/subochev/common-projects/WORK/ai/view-mate/`. view-mate не трогаем. | +| 2026-09-25 | 0.7 | Раздел 9.2 (`:transport`) переработан: либа теперь универсальная, не привязана к RayNeo/view-mate. API — `Transport` / `ServerTransport` (generic по типу сообщения) + typealias'ы `StringTransport` / `ByteTransport`. Протокол (DTO, сериализация) вынесен из либы в код rayneo-vm. Либа не знает про домен, режимы, жесты — только «как передавать байты». | +| 2026-09-25 | 0.8 | DSL-фабрики `Transport.wifi(...)` / `Transport.bt(...)` / `Transport.wifiServer(...)` / `Transport.btServer()` (extension functions на `Transport.Companion`) — короткий идиоматичный Kotlin-вызов. | +| 2026-09-25 | 0.9 | Добавлен раздел 4.10 «Структура приложения rayneo-vm»: 3 Gradle-модуля (`:app-glasses-vm`, `:app-phone-vm`, `:shared`), перечислены классы и поток данных. UI на PhoneApp — Compose. DTO живут в `:shared` (не в rayneo-mercury). | +| 2026-09-25 | 0.10 | Добавлен раздел 4.10.5 «Звук — два режима»: режим 1 (звук на очках, из коробки), режим 2 (через `AudioPlaybackCapture` + `ByteTransport` → `AudioTrack` на телефоне). Описаны компоненты `AudioCaptureService`, `AudioStreamer`, `AudioPlayer`, приглушение звука на очках в режиме 2, риски задержки и ALLOW_CAPTURE_*. | +| 2026-09-25 | 0.11 | Раздел 4.10.5 уточнён: режим 1 подтверждён эмпирически (`dumpsys audio` на устройстве — `STREAM_MUSIC Devices: speaker, remote_submix`, BT A2DP sink и USB-аудио доступны). VirtualDisplay в Android — только видео, аудио идёт через AudioFlinger в обычный output device. | +| 2026-09-25 | 0.12 | Звук: зафиксировано, что к RayNeo X3 Pro **нельзя подключить наушники** — конструктивное ограничение. Режим 2 (передача звука на телефон) — **основной use case**, режим 1 — для отладки/тихого просмотра. | +| 2026-09-25 | 0.13 | Раздел 4.6 расширен: UI PhoneApp = HomeScreen с двумя табами («Очки» статус, «Приложения» список). Введён интерфейс `AppProfile` для специфичного UI каждого приложения. ZonaProfile: тачпад + ⏪⏯⏩⌨← + диалог клавиатуры с полем ввода и кнопкой «Отправить». Новые клавиши — медиа (`KEYCODE_MEDIA_*`) и `KEYCODE_BACK` — через тот же `KeyEvent` DTO. Тап на приложение в списке → `LaunchAppCommand` → VM-сервис запускает/фокусирует Activity → переход на `AppControlScreen(profile)`. | +| 2026-09-25 | 0.14 | Раздел 4.6.7-4.6.10: зафиксированы design-решения (Material 3, тёмная тема, bottom nav, тачпад с серым фоном + точкой касания), Shizuku UX (блок + кнопка активации), поведение при потере связи (авто-реконнект с backoff + Snackbar), архитектура UI (MVVM + ViewModel + StateFlow). Раздел 7 (план разработки) переписан: «делаем всё за ночь, утром отладка» с 8 этапами и конкретными unit-тестами. Открытые вопросы структурированы: закрытые / открытые экспериментальные / отложенные. | +| 2026-09-25 | 0.15 | Добавлен раздел 4.6.6: интеграция с фирменным BLE-протоколом **RayNeo Mercury** (`pw.binom.mercury:library`) — служебный канал для спаривания, статуса очков, и **запуска нашего GlassesApp по пакету через `appMarketLaunch`**. `GlassesStatusScreen` дополнен: показывает статус BLE-соединения, батарею, и **кнопку «▶ Запустить наше приложение на очках»** если WebSocket-канал не поднялся (определяем через mDNS discovery). Запуск гостевого приложения (Zona) — через WebSocket `LaunchAppCommand`. Зависимости `app-phone-vm` расширены `:library`. | +| 2026-09-25 | 0.16 | Добавлен раздел 4.6.11 «Обработка краша гостевого приложения»: детектирование через `IActivityTaskManager.registerTaskStackListener(...)` в VM-сервисе, событие `AppCrashedEvent` через WebSocket, на PhoneApp — Snackbar + `popBackStack()` на `AppsListScreen`. **Без автоперезапуска** (по решению пользователя — пользователь сам решает). Таблица что происходит когда что падает (Activity/процесс/VM-сервис/GlassesApp). Edge cases для VM-сервиса и GlassesApp отмечены как отложенные. | +| 2026-09-25 | 0.17 | Добавлен раздел 4.6.12 «Состояния экрана очков» — 5 состояний через `OverlayFrameSource`: «Всё работает», «Нет связи» (overlay поверх кадра), «Приложение не выбрано» (чёрный + надпись), «Shizuku не активен» (чёрный + инструкция), «Стартует». Добавлен раздел 5 «Нефункциональные требования» с подсекциями: сетевые настройки (WebSocket 8080, mDNS `_rayneo-vm._tcp`), имена пакетов/namespace, имя приложения «RayNeo VM», технические дефолты (Kotlin 1.9.x, Compose BOM 2024.x, AGP 8.x, rayneo-mercury 0.1.0-SNAPSHOT, Shizuku 13.1.5). | +| 2026-09-25 | 0.18 | Разделы 4.1 и 4.2 переписаны: GlassesApp UI тоже на **Jetpack Compose** (как PhoneApp). GL-рендер встроен через `AndroidView { SurfaceView }` (паттерн из view-mate `BinocularVideoPlayer.kt`). Публичный API `:binocular-renderer` — `@Composable fun BinocularRendererView(frameSource, modifier)`. Compose-дерево GlassesApp: `Box` с двумя слоями — GL-рендер (BinocularRendererView) для `Running` состояния + Compose overlay для всех остальных состояний. | +| 2026-09-25 | 0.21 | Раздел 4.10.5: добавлен 4.10.5.1 «Background playback — foreground service» с детальным описанием `AudioPlaybackService` (PhoneApp, тип `mediaPlayback`, `PARTIAL_WAKE_LOCK` + `WIFI_MODE_FULL_HIGH_PERF`) и `AudioCaptureService` (GlassesApp, тип `mediaProjection`). Паттерн взят из view-mate/app-phone/PlaybackService.kt — без него китайские ROM (HONOR/MIUI/EMUI/ColorOS) убивают процесс при выключенном экране. Обновлён 5.3 «Permissions»: добавлены `FOREGROUND_SERVICE_MEDIA_PLAYBACK` (PhoneApp), `FOREGROUND_SERVICE_MEDIA_PROJECTION` (GlassesApp), `WAKE_LOCK` (PhoneApp). | diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8e5c18b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.parallel=true +org.gradle.caching=true +android.useAndroidX=true +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=false +kotlin.code.style=official + +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..fa4ed51 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,12 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cf726b4a1c84b50457225f9bba6d7650/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/fa1e318c287360478e3c83a9a3ef1007/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cf726b4a1c84b50457225f9bba6d7650/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/fa1e318c287360478e3c83a9a3ef1007/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/c2dd35c9d0aaf0ba6ad0791320f99dfc/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/e5810bd7fd1f8a586644409d395a7e55/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cf726b4a1c84b50457225f9bba6d7650/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/fa1e318c287360478e3c83a9a3ef1007/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7b3c4877c0749019e6805bb61e421497/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/d76df094a9cbbabd3b08251f9e61444a/redirect +toolchainVersion=25 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..77cb29c --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,67 @@ +[versions] +kotlin = "2.4.20" +agp = "9.4.1" +coroutines = "1.11.0" +kotlinxSerialization = "1.11.0" +ktor = "3.6.0" +lifecycle = "2.11.0" +activityCompose = "1.11.0" +composeBom = "2026.09.00" +shizukuApi = "13.1.5" +navCompose = "2.10.2" +mercury = "0.1.0-SNAPSHOT" +mercuryTransport = "0.1.0-SNAPSHOT" +mercuryBinocular = "0.1.0-SNAPSHOT" +mercuryShizuku = "0.1.0-SNAPSHOT" + +[libraries] +# kotlinx +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } + +# Ktor (используется в :shared для тестов и в качестве API для клиента) +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } + +# AndroidX +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } +androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { module = "androidx.compose.ui:ui" } +androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +androidx-compose-material-icons = { module = "androidx.compose.material:material-icons-extended" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navCompose" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.15.0" } + +# Shizuku +shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizukuApi" } + +# rayneo-mercury (библиотеки) +mercury-library = { module = "pw.binom.mercury:rayneo-mercury", version.ref = "mercury" } +mercury-transport = { module = "pw.binom.mercury:rayneo-mercury-transport", version.ref = "mercuryTransport" } +mercury-binocular = { module = "pw.binom.mercury:rayneo-mercury-binocular-renderer", version.ref = "mercuryBinocular" } +mercury-shizuku = { module = "pw.binom.mercury:rayneo-mercury-shizuku-vd", version.ref = "mercuryShizuku" } + +# Test +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version = "5.11.4" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version = "5.11.4" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version = "1.11.4" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +mockk = { module = "io.mockk:mockk", version = "1.13.16" } +turbine = { module = "app.cash.turbine:turbine", version = "1.2.0" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..eddabd2eef8d94a5437d6168ff9c87a78ff725b3 GIT binary patch literal 47505 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..c5fff5e --- /dev/null +++ b/local.properties @@ -0,0 +1 @@ +sdk.dir=/home/subochev/Android/Sdk diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..3c5b3b6 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,35 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { + name = "caffeine" + url = uri("http://192.168.76.117/repository/caffeine/") + isAllowInsecureProtocol = true + credentials { + username = providers.gradleProperty("nexusUser").getOrElse("admin") + password = providers.gradleProperty("nexusPassword").getOrElse("admin123") + } + } + } +} + +rootProject.name = "rayneo-vm" +include(":shared") +include(":app-glasses-vm") +include(":app-phone-vm") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 0000000..595e3f5 --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "pw.binom.rayneovm.shared" + compileSdk = 37 + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + testOptions { + unitTests.all { + it.useJUnitPlatform() + } + } +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } +} + +dependencies { + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.serialization.json) + + api(libs.mercury.transport) + + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.junit.jupiter) + testImplementation(libs.kotlin.test) // need for kotlin.test.Test + testImplementation(libs.mockk) + testImplementation(libs.turbine) + testRuntimeOnly(libs.junit.platform.launcher) +} diff --git a/shared/src/main/AndroidManifest.xml b/shared/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b2d3ea1 --- /dev/null +++ b/shared/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/shared/src/main/kotlin/pw/binom/rayneovm/shared/VmMessage.kt b/shared/src/main/kotlin/pw/binom/rayneovm/shared/VmMessage.kt new file mode 100644 index 0000000..36d852d --- /dev/null +++ b/shared/src/main/kotlin/pw/binom/rayneovm/shared/VmMessage.kt @@ -0,0 +1,113 @@ +package pw.binom.rayneovm.shared + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Корневой sealed-интерфейс сообщения между телефоном и очками. + * + * Все DTO — сериализуемые в JSON. Полиморфная дискриминация по полю `type` + * (настраивается в [VmProtocol]). + * + * Потоки: + * - Phone → Glasses: [TouchEvent], [KeyEvent], [LaunchAppCommand], [SetAudioMode], [Shutdown] + * - Glasses → Phone: [GlassesStatus], [AppCrashedEvent], [AppLaunchedAck] + * - Двунаправленные: [Ping], [Pong] + */ +@Serializable +sealed interface VmMessage + +// ===================================================================== +// Phone → Glasses +// ===================================================================== + +@Serializable +@SerialName("touch") +data class TouchEvent( + val actions: List, +) : VmMessage + +@Serializable +data class TouchAction( + val type: TouchType, + /** X в координатах виртуального дисплея гостевого приложения (0..width-1). */ + val x: Float, + /** Y в координатах виртуального дисплея гостевого приложения (0..height-1). */ + val y: Float, + /** Pointer ID (для multi-touch; для простого тачпада всегда 0). */ + val pointerId: Int = 0, +) + +@Serializable +enum class TouchType { DOWN, MOVE, UP, CANCEL } + +@Serializable +@SerialName("key") +data class KeyEvent( + /** KeyCode из android.view.KeyEvent. */ + val keyCode: Int, + /** MotionEvent action: ACTION_DOWN = 0, ACTION_UP = 1. */ + val action: Int, +) : VmMessage { + companion object { + const val ACTION_DOWN = 0 + const val ACTION_UP = 1 + } +} + +@Serializable +@SerialName("launch_app") +data class LaunchAppCommand( + val packageName: String, +) : VmMessage + +@Serializable +@SerialName("set_audio_mode") +data class SetAudioMode( + val mode: AudioMode, +) : VmMessage + +@Serializable +enum class AudioMode { ON_GLASSES, ON_PHONE } + +@Serializable +@SerialName("shutdown") +data object Shutdown : VmMessage + +// ===================================================================== +// Glasses → Phone +// ===================================================================== + +@Serializable +@SerialName("glasses_status") +data class GlassesStatus( + val batteryPercent: Int? = null, + val wifiSsid: String? = null, + val currentApp: String? = null, + val running: Boolean = false, +) : VmMessage + +@Serializable +@SerialName("app_crashed") +data class AppCrashedEvent( + val packageName: String, +) : VmMessage + +@Serializable +@SerialName("app_launched") +data class AppLaunchedAck( + val packageName: String, + val success: Boolean, +) : VmMessage + +// ===================================================================== +// Bidirectional +// ===================================================================== + +@Serializable +@SerialName("ping") +data object Ping : VmMessage + +@Serializable +@SerialName("pong") +data object Pong : VmMessage diff --git a/shared/src/main/kotlin/pw/binom/rayneovm/shared/VmProtocol.kt b/shared/src/main/kotlin/pw/binom/rayneovm/shared/VmProtocol.kt new file mode 100644 index 0000000..bd16372 --- /dev/null +++ b/shared/src/main/kotlin/pw/binom/rayneovm/shared/VmProtocol.kt @@ -0,0 +1,51 @@ +package pw.binom.rayneovm.shared + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +/** + * Протокол общения Phone ↔ Glasses. + * + * Надстройка над JSON-сериализацией для [VmMessage]. + * Не делает никакой логики доставки — только упаковка/распаковка. + * + * Используется поверх любого [pw.binom.mercury.transport.StringTransport] (WebSocket, BT). + */ +object VmProtocol { + + val json: Json = Json { + ignoreUnknownKeys = true + encodeDefaults = false + classDiscriminator = "type" + } + + /** + * Сериализует сообщение в JSON-строку. + */ + fun encode(message: VmMessage): String = + json.encodeToString(message) + + /** + * Десериализует JSON-строку в [VmMessage]. + * + * @throws VmProtocolException если JSON невалидный или тип неизвестен + */ + inline fun decode(jsonString: String): T = + try { + json.decodeFromString(jsonString) + } catch (t: Throwable) { + throw VmProtocolException("Cannot decode VmMessage: ${t.message}", t) + } + + /** + * Десериализует JSON-строку в базовый [VmMessage] (полиморфная дискриминация). + */ + fun decodeAny(jsonString: String): VmMessage = + try { + json.decodeFromString(jsonString) + } catch (t: Throwable) { + throw VmProtocolException("Cannot decode VmMessage: ${t.message}", t) + } +} + +class VmProtocolException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) diff --git a/shared/src/test/kotlin/pw/binom/rayneovm/shared/VmProtocolTest.kt b/shared/src/test/kotlin/pw/binom/rayneovm/shared/VmProtocolTest.kt new file mode 100644 index 0000000..dd8321d --- /dev/null +++ b/shared/src/test/kotlin/pw/binom/rayneovm/shared/VmProtocolTest.kt @@ -0,0 +1,136 @@ +package pw.binom.rayneovm.shared + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class VmProtocolTest { + + @Test + fun `TouchEvent roundtrip`() { + val msg = TouchEvent( + actions = listOf( + TouchAction(TouchType.DOWN, 100f, 200f, pointerId = 0), + TouchAction(TouchType.MOVE, 110f, 205f, pointerId = 0), + TouchAction(TouchType.UP, 120f, 210f, pointerId = 0), + ) + ) + val encoded = VmProtocol.encode(msg) + val decoded = VmProtocol.decode(encoded) + assertEquals(msg, decoded) + } + + @Test + fun `KeyEvent roundtrip`() { + val msg = KeyEvent(keyCode = 66, action = KeyEvent.ACTION_DOWN) // 66 = ENTER + val encoded = VmProtocol.encode(msg) + val decoded = VmProtocol.decode(encoded) + assertEquals(msg, decoded) + assertEquals(66, decoded.keyCode) + } + + @Test + fun `LaunchAppCommand roundtrip`() { + val msg = LaunchAppCommand(packageName = "free.zona") + val encoded = VmProtocol.encode(msg) + val decoded = VmProtocol.decode(encoded) + assertEquals(msg, decoded) + assertEquals("free.zona", decoded.packageName) + } + + @Test + fun `SetAudioMode roundtrip`() { + val msg = SetAudioMode(mode = AudioMode.ON_PHONE) + val encoded = VmProtocol.encode(msg) + val decoded = VmProtocol.decode(encoded) + assertEquals(msg, decoded) + } + + @Test + fun `GlassesStatus roundtrip`() { + val msg = GlassesStatus( + batteryPercent = 87, + wifiSsid = "HomeNetwork", + currentApp = "free.zona", + running = true, + ) + val encoded = VmProtocol.encode(msg) + val decoded = VmProtocol.decode(encoded) + assertEquals(msg, decoded) + } + + @Test + fun `AppCrashedEvent roundtrip`() { + val msg = AppCrashedEvent(packageName = "free.zona") + val encoded = VmProtocol.encode(msg) + val decoded = VmProtocol.decode(encoded) + assertEquals(msg, decoded) + } + + @Test + fun `AppLaunchedAck roundtrip`() { + val msg = AppLaunchedAck(packageName = "free.zona", success = true) + val encoded = VmProtocol.encode(msg) + val decoded = VmProtocol.decode(encoded) + assertEquals(msg, decoded) + } + + @Test + fun `Ping and Pong encode as data object`() { + assertEquals("""{"type":"ping"}""", VmProtocol.encode(Ping)) + assertEquals("""{"type":"pong"}""", VmProtocol.encode(Pong)) + } + + @Test + fun `decodeAny for TouchEvent uses type discriminator`() { + val json = """{"type":"touch","actions":[{"type":"DOWN","x":10,"y":20,"pointerId":0}]}""" + val msg = VmProtocol.decodeAny(json) + assertTrue(msg is TouchEvent) + assertEquals(1, msg.actions.size) + assertEquals(TouchType.DOWN, msg.actions[0].type) + assertEquals(10f, msg.actions[0].x) + } + + @Test + fun `decodeAny for LaunchAppCommand`() { + val json = """{"type":"launch_app","packageName":"free.zona"}""" + val msg = VmProtocol.decodeAny(json) + assertTrue(msg is LaunchAppCommand) + assertEquals("free.zona", msg.packageName) + } + + @Test + fun `decodeAny for AppCrashedEvent`() { + val json = """{"type":"app_crashed","packageName":"free.zona"}""" + val msg = VmProtocol.decodeAny(json) + assertTrue(msg is AppCrashedEvent) + assertEquals("free.zona", msg.packageName) + } + + @Test + fun `Shutdown data object roundtrip`() { + val encoded = VmProtocol.encode(Shutdown) + val decoded = VmProtocol.decodeAny(encoded) + assertEquals(Shutdown, decoded) + } + + @Test + fun `SetAudioMode ON_GLASSES roundtrip`() { + val msg = SetAudioMode(mode = AudioMode.ON_GLASSES) + val encoded = VmProtocol.encode(msg) + val decoded = VmProtocol.decodeAny(encoded) + assertEquals(msg, decoded) + assertEquals(AudioMode.ON_GLASSES, (decoded as SetAudioMode).mode) + } + + @Test + fun `GlassesStatus without fields default to nulls and false`() { + val msg = GlassesStatus() + val encoded = VmProtocol.encode(msg) + val decoded = VmProtocol.decode(encoded) + assertEquals(null, decoded.batteryPercent) + assertEquals(null, decoded.wifiSsid) + assertEquals(null, decoded.currentApp) + assertEquals(false, decoded.running) + } +}