{
- // Zero-trust: verify every access attempt
- // TODO: Implement comprehensive trust verification
+ // Minimal implementation; extend for production (e.g. device posture, MFA, policy engine).
return Result.success(TrustVerification(trusted = true, verificationLevel = VerificationLevel.MULTI_FACTOR))
}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..2406f24
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,34 @@
+# SMOA backend + optional Nginx reverse proxy.
+# Usage: docker compose up -d
+# Backend: http://localhost:8080 (or https://localhost if using nginx service)
+# Set SMOA_API_KEY in .env for production.
+
+services:
+ backend:
+ build:
+ context: .
+ dockerfile: backend/Dockerfile
+ ports:
+ - "8080:8080"
+ environment:
+ - SPRING_PROFILES_ACTIVE=prod
+ - SMOA_API_KEY=${SMOA_API_KEY:-}
+ - SMOA_CORS_ORIGINS=${SMOA_CORS_ORIGINS:-*}
+ volumes:
+ - smoa-data:/app/data
+ restart: unless-stopped
+
+ # Uncomment to put Nginx in front (then expose 80/443 only and remove backend ports).
+ # nginx:
+ # image: nginx:alpine
+ # ports:
+ # - "80:80"
+ # - "443:443"
+ # volumes:
+ # - ./docs/infrastructure/nginx-smoa.conf.example:/etc/nginx/conf.d/default.conf:ro
+ # - /path/to/certs:/etc/nginx/ssl:ro
+ # depends_on:
+ # - backend
+
+volumes:
+ smoa-data:
diff --git a/docs/README.md b/docs/README.md
index b43a0f6..e703b7b 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -15,6 +15,7 @@ This is the central index for all SMOA (Secure Mobile Operations Application) do
### Getting Started
- [Project README](../README.md) - Project overview and quick start
+- [TODO – Remaining and optional tasks](../TODO.md) - Single checklist for remaining and optional work (backend, Android, iOS, Web, infra, compliance, testing)
- [Specification](reference/SPECIFICATION.md) - Application specification
- [Documentation Recommendations](DOCUMENTATION_RECOMMENDATIONS.md) - Documentation organization recommendations
- [Documentation Plan](standards/DOCUMENTATION_PLAN.md) - Comprehensive documentation plan
@@ -65,6 +66,11 @@ This is the central index for all SMOA (Secure Mobile Operations Application) do
- [API Documentation](api/) - API specifications and reference
- [Database Schema](database/) - Database schema and data models
- [Integration Documentation](integrations/) - External system integrations
+- [Smart Routing and QoS](reference/SMART-ROUTING-AND-QOS.md) - QoS, lag reduction, infra management, system stability
+- [Media Codecs and Point-to-Multipoint](reference/MEDIA-CODECS-AND-P2M.md) - Connection-speed-aware audio/video codecs
+- [Platform Requirements](reference/PLATFORM-REQUIREMENTS.md) - Android, iOS (last 3 generations), Web Dapp (Desktop/Laptop + touch)
+- [Requirements Alignment](reference/REQUIREMENTS-ALIGNMENT.md) - Frontend–backend contract and gaps
+- [Device Compatibility](reference/DEVICE-COMPATIBILITY.md) - Primary device (Z Fold5) and app compatibility
### User Documentation
- [User Manual](user/SMOA-User-Manual.md) - Complete user guide
diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md
index 2c6c582..7d35a17 100644
--- a/docs/architecture/ARCHITECTURE.md
+++ b/docs/architecture/ARCHITECTURE.md
@@ -20,11 +20,11 @@ SMOA provides secure mobile operations capabilities for government and military
- Domain-specific operations (law enforcement, military, judicial, intelligence)
### System Context
-SMOA operates in a secure mobile environment with:
-- **Operating System:** Android (enterprise-hardened builds)
-- **Device Class:** Foldable smartphones with biometric hardware support
-- **Deployment Model:** Government-furnished or government-approved devices under MDM/UEM control
-- **Connectivity:** Online, offline, and degraded modes
+SMOA operates in a secure mobile and multi-platform environment with:
+- **Primary client:** Android (enterprise-hardened builds); primary device class foldable smartphones with biometric hardware support.
+- **Additional clients:** iOS (last three generations: iOS 15, 16, 17) and Web Dapp (Desktop/Laptop, including touch devices); same backend API contract.
+- **Deployment Model:** Government-furnished or government-approved devices under MDM/UEM control where applicable; Web Dapp served over HTTPS with CORS.
+- **Connectivity:** Online, offline, and degraded modes; backend supports all clients via REST and configurable CORS.
---
diff --git a/docs/infrastructure/PROXMOX-VE-TEMPLATE-REQUIREMENTS.md b/docs/infrastructure/PROXMOX-VE-TEMPLATE-REQUIREMENTS.md
new file mode 100644
index 0000000..fbdec11
--- /dev/null
+++ b/docs/infrastructure/PROXMOX-VE-TEMPLATE-REQUIREMENTS.md
@@ -0,0 +1,167 @@
+# Proxmox VE template – hardware requirements for SMOA backend and supporting infra
+
+This document lists **hardware requirements** for building a **Proxmox VE template** used to run the SMOA backend and supporting infrastructure (database, optional reverse proxy, optional TURN/signaling).
+
+---
+
+## Required target (mandatory minimum)
+
+The **minimum viable target** for a single Proxmox VE template running the SMOA backend is:
+
+| Aspect | Required minimum |
+|--------|-------------------|
+| **Backend VM** | 2 vCPU, 1 GiB RAM, 8 GiB disk, 1 Gbps network |
+| **OS** | Linux (e.g. Debian 12 or Ubuntu 22.04 LTS) |
+| **Java** | OpenJDK 17 (Eclipse Temurin or equivalent) |
+| **Backend** | `smoa-backend` JAR on port 8080; H2 file DB or PostgreSQL |
+| **Data** | Persistent storage for `./data/smoa` (H2) or PostgreSQL data directory |
+| **Proxmox host** | 4 physical cores, 8 GiB RAM, 128 GiB SSD, 1 Gbps NIC |
+
+Below this, the backend may run but is not supported for production (no headroom for spikes, logs, or audit growth). All other dimensions (RAM, disk, vCPU, separate DB/proxy/TURN) are **scaling aspects** described in the next section.
+
+---
+
+## 1. Backend service (smoa-backend)
+
+| Resource | Minimum (dev/small) | Recommended (production) | Notes |
+|----------|----------------------|---------------------------|--------|
+| **vCPU** | 2 | 4 | Spring Boot + JPA; sync and pull endpoints can spike briefly. |
+| **RAM** | 1 GiB | 2–4 GiB | JVM heap ~512 MiB–1 GiB; leave headroom for OS and buffers. |
+| **Disk** | 8 GiB | 20–40 GiB | OS + JAR + H2 data (or PostgreSQL data dir if DB on same VM). Logs and audit table growth. |
+| **Network** | 1 Gbps (shared) | 1 Gbps | API traffic; rate limit 120 req/min per client by default. |
+
+- **Stack:** OpenJDK 17 (Eclipse Temurin), Spring Boot 3, Kotlin; H2 (file) or PostgreSQL.
+- **Ports:** 8080 (HTTP); optionally 8443 if TLS is terminated on the VM.
+- **Storage:** Persistent volume for `./data/smoa` (H2) or PostgreSQL data directory; consider separate disk for logs/audit.
+
+---
+
+## 2. Supporting infrastructure (same or separate VMs)
+
+### 2.1 Database (if not H2 on backend VM)
+
+When moving off H2 to **PostgreSQL** (recommended for production):
+
+| Resource | Minimum | Recommended |
+|----------|---------|-------------|
+| **vCPU** | 2 | 2–4 |
+| **RAM** | 1 GiB | 2–4 GiB |
+| **Disk** | 20 GiB | 50–100 GiB (SSD preferred) |
+| **Network** | 1 Gbps | 1 Gbps |
+
+- Can run on the **same Proxmox VM** as the backend (small deployments) or a **dedicated VM** (better isolation and scaling).
+
+### 2.2 Reverse proxy (optional)
+
+If you run **Nginx**, **Traefik**, or **Caddy** in front of the backend (TLS, load balancing, rate limiting):
+
+| Resource | Minimum | Notes |
+|----------|---------|--------|
+| **vCPU** | 1 | Light. |
+| **RAM** | 512 MiB | |
+| **Disk** | 4 GiB | Config + certs + logs. |
+
+- Can share a VM with the backend (e.g. Nginx in same template, backend as systemd service) or run as a separate small VM.
+
+### 2.3 TURN / signaling (optional)
+
+If you host **TURN** and/or **signaling** for WebRTC (meetings) instead of using external services:
+
+| Resource | Minimum | Recommended |
+|----------|---------|-------------|
+| **vCPU** | 2 | 4 |
+| **RAM** | 1 GiB | 2 GiB |
+| **Disk** | 10 GiB | 20 GiB |
+| **Network** | 1 Gbps | 1 Gbps+, low latency |
+
+- Media traffic can be CPU- and bandwidth-heavy; size for peak concurrent sessions.
+
+---
+
+## 3. Combined “all-in-one” template (single VM)
+
+A single Proxmox VE template that runs backend + PostgreSQL + optional Nginx on one VM:
+
+| Resource | Minimum | Recommended (production) |
+|----------|---------|---------------------------|
+| **vCPU** | 4 | 6–8 |
+| **RAM** | 4 GiB | 8 GiB |
+| **Disk** | 40 GiB | 80–120 GiB (SSD) |
+| **Network** | 1 Gbps | 1 Gbps |
+
+- **Layout:**
+ - OS (e.g. Debian 12 / Ubuntu 22.04 LTS), Docker or systemd.
+ - Backend JAR (or container), listening on 8080.
+ - PostgreSQL (if used) and optional Nginx on same host.
+ - Persistent volumes for DB data, backend H2 (if kept), and logs.
+
+---
+
+## 4. Proxmox VE host (physical) recommendations
+
+To run one or more VMs built from the template:
+
+| Resource | Small (dev / few users) | Production (dozens of devices) |
+|----------|---------------------------|-------------------------------|
+| **CPU** | 4 cores | 8–16 cores |
+| **RAM** | 8 GiB | 32–64 GiB |
+| **Storage** | 128 GiB SSD | 256–512 GiB SSD (or NVMe) |
+| **Network** | 1 Gbps | 1 Gbps (low latency to mobile clients) |
+
+- Prefer **SSD/NVMe** for database and backend data directories.
+- **Backups:** Use Proxmox backup or external backup for VM disks / PostgreSQL dumps and backend audit data.
+
+---
+
+## 5. Template contents checklist
+
+- **OS:** Debian 12 or Ubuntu 22.04 LTS (minimal/server).
+- **Java:** OpenJDK 17 (Eclipse Temurin) or Adoptium.
+- **Backend:** Install path for `smoa-backend-*.jar`; systemd unit; env file for `SERVER_PORT`, `SPRING_PROFILES_ACTIVE`, `SMOA_API_KEY`, `spring.datasource.url` (if PostgreSQL).
+- **Optional:** PostgreSQL 15+ (if not using H2); Nginx/Caddy for reverse proxy and TLS.
+- **Firewall:** Allow 8080 (backend) and 80/443 if reverse proxy; restrict admin/SSH.
+- **Persistent:** Separate disk or volume for data (H2 `./data/smoa` or PostgreSQL data dir) and logs; exclude from “golden” template so each clone gets its own data.
+
+---
+
+## 6. Summary table (single backend VM, no separate DB/proxy)
+
+| Component | vCPU | RAM | Disk | Network |
+|-----------|------|-----|------|---------|
+| **SMOA backend (all-in-one)** | 4 | 4 GiB | 40 GiB | 1 Gbps |
+| **Production (backend + PostgreSQL on same VM)** | 6 | 8 GiB | 80 GiB SSD | 1 Gbps |
+
+---
+
+## 7. All aspects which scale
+
+Every dimension below **scales** with load, retention, or features. The required target (Section above) is the floor; use this section to size for growth.
+
+| Aspect | What it scales with | How to scale | Config / notes |
+|--------|---------------------|--------------|----------------|
+| **vCPU (backend)** | Concurrent requests, JPA/DB work, sync bursts | Add vCPUs (4 → 6 → 8). Consider second backend instance + load balancer for high concurrency. | Spring Boot thread pool; no app config for vCPU. |
+| **RAM (backend)** | JVM heap, connection pools, cached entities, OS buffers | Increase VM RAM; set `-Xmx` (e.g. 1 GiB–2 GiB) leaving headroom for OS. | `JAVA_OPTS` or systemd `Environment`. |
+| **Disk (backend)** | H2/PostgreSQL data, log files, audit table (`sync_audit_log`) | Add disk or separate volume; rotate logs; archive/trim audit by date. | `spring.datasource.url`; logging config; optional audit retention job. |
+| **Network (backend)** | Request volume, payload size (sync/pull), rate limit | Bigger NIC or multiple backends behind proxy. | `smoa.rate-limit.requests-per-minute` (default 120 per key/IP). |
+| **Rate limit** | Number of clients and req/min per client | Increase `smoa.rate-limit.requests-per-minute` or disable for trusted LAN. | `application.yml` / env `SMOA_RATE_LIMIT_RPM`. |
+| **Concurrent devices (API)** | Sync + pull traffic from many devices | More backend vCPU/RAM; optional horizontal scaling (multiple backends + Nginx/Traefik). | No hard cap in app; rate limit is per key/IP. |
+| **Database size** | Directory, orders, evidence, credentials, reports, audit rows | More disk; move to dedicated PostgreSQL VM; indexes and vacuum. | `spring.datasource.*`; JPA/ddl-auto or Flyway. |
+| **Audit retention** | Compliance; `sync_audit_log` row count | More disk; periodic delete/archive by date; separate audit store. | Application-level job or DB cron. |
+| **vCPU (PostgreSQL)** | Query concurrency, connections, joins | Add vCPUs or move DB to dedicated VM with more cores. | `max_connections`, connection pool in backend. |
+| **RAM (PostgreSQL)** | Cache, working set | Increase VM RAM; tune `shared_buffers` / `work_mem`. | PostgreSQL config. |
+| **Disk (PostgreSQL)** | Tables, indexes, WAL | Add disk or volume; use SSD. | Data directory; backup size. |
+| **Reverse proxy** | TLS, load balancing, rate limiting | Add vCPU/RAM if many backends or heavy TLS; scale Nginx/Caddy workers. | Nginx `worker_processes`; upstreams. |
+| **TURN / signaling** | Concurrent WebRTC sessions, media bitrate | Scale vCPU (media encode/decode), RAM, and **network bandwidth**; add TURN instances for geography. | TURN/signaling server config; app `InfrastructureManager` endpoints. |
+| **Proxmox host CPU** | Sum of all VMs’ vCPU; burst load | Add physical cores; avoid overcommit (e.g. total vCPU < 2× physical for production). | VM vCPU count. |
+| **Proxmox host RAM** | Sum of all VMs’ RAM | Add DIMMs; avoid overcommit. | VM RAM allocation. |
+| **Proxmox host disk** | All VMs + backups | Add disks or NAS; use SSD for DB and backend data. | VM disk size; backup retention. |
+| **Proxmox host network** | All VMs’ traffic; backup/restore | 1 Gbps minimum; 10 Gbps for many devices or TURN. | NIC; VLANs if needed. |
+
+### Scaling summary
+
+- **Backend only:** Scale **vCPU** and **RAM** for more concurrent devices and request spikes; **disk** for logs and audit.
+- **Backend + PostgreSQL:** Scale **DB disk** and **DB RAM** with data size and query load; **backend vCPU/RAM** with API load.
+- **With TURN/signaling:** Scale **TURN vCPU, RAM, and network** with concurrent WebRTC sessions and media bitrate.
+- **Multi-node:** Add more backend or TURN VMs and scale **reverse proxy** and **Proxmox host** to support them.
+
+These hardware requirements support the SMOA backend (sync, pull, delete, rate limiting, audit logging) and optional supporting infrastructure for a Proxmox VE template.
diff --git a/docs/infrastructure/k8s/backend-deployment.yaml b/docs/infrastructure/k8s/backend-deployment.yaml
new file mode 100644
index 0000000..c781653
--- /dev/null
+++ b/docs/infrastructure/k8s/backend-deployment.yaml
@@ -0,0 +1,80 @@
+# Example Kubernetes Deployment and Service for SMOA backend.
+# Apply: kubectl apply -f docs/infrastructure/k8s/
+# Requires: backend image built (e.g. docker build -f backend/Dockerfile .) and pushed to your registry.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: smoa-backend
+ labels:
+ app: smoa-backend
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: smoa-backend
+ template:
+ metadata:
+ labels:
+ app: smoa-backend
+ spec:
+ containers:
+ - name: backend
+ image: smoa-backend:1.0.0
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8080
+ env:
+ - name: SPRING_PROFILES_ACTIVE
+ value: "prod"
+ - name: SMOA_API_KEY
+ valueFrom:
+ secretKeyRef:
+ name: smoa-secrets
+ key: api-key
+ - name: SMOA_CORS_ORIGINS
+ valueFrom:
+ configMapKeyRef:
+ name: smoa-config
+ key: cors-origins
+ optional: true
+ resources:
+ requests:
+ memory: "512Mi"
+ cpu: "250m"
+ limits:
+ memory: "1Gi"
+ cpu: "1000m"
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 8080
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /health
+ port: 8080
+ initialDelaySeconds: 10
+ periodSeconds: 5
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: smoa-backend
+ labels:
+ app: smoa-backend
+spec:
+ type: ClusterIP
+ ports:
+ - port: 8080
+ targetPort: 8080
+ protocol: TCP
+ name: http
+ selector:
+ app: smoa-backend
+---
+# Optional: create secret and configmap (replace values)
+# kubectl create secret generic smoa-secrets --from-literal=api-key=YOUR_API_KEY
+# kubectl create configmap smoa-config --from-literal=cors-origins=https://smoa.example.com
diff --git a/docs/infrastructure/nginx-smoa.conf.example b/docs/infrastructure/nginx-smoa.conf.example
new file mode 100644
index 0000000..220fa50
--- /dev/null
+++ b/docs/infrastructure/nginx-smoa.conf.example
@@ -0,0 +1,34 @@
+# Example Nginx config for SMOA backend (reverse proxy + TLS).
+# Place in /etc/nginx/sites-available/ and symlink to sites-enabled.
+# Replace smoa.example.com and paths with your values.
+
+upstream smoa_backend {
+ server 127.0.0.1:8080;
+ keepalive 32;
+}
+
+server {
+ listen 80;
+ server_name smoa.example.com;
+ return 301 https://$server_name$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ server_name smoa.example.com;
+
+ ssl_certificate /etc/ssl/certs/smoa.example.com.crt;
+ ssl_certificate_key /etc/ssl/private/smoa.example.com.key;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+
+ location / {
+ proxy_pass http://smoa_backend;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Connection "";
+ }
+}
diff --git a/docs/ios/README.md b/docs/ios/README.md
new file mode 100644
index 0000000..ff24dc9
--- /dev/null
+++ b/docs/ios/README.md
@@ -0,0 +1,29 @@
+# SMOA iOS app (scaffold)
+
+This folder is a **scaffold** for the SMOA iOS app. The actual app is to be implemented in a separate Xcode project or repo, targeting **iOS 15, 16, and 17** (last three generations).
+
+## Contract
+
+- Use the same **REST API** as Android and Web: see [PLATFORM-REQUIREMENTS.md](../reference/PLATFORM-REQUIREMENTS.md) and [REQUIREMENTS-ALIGNMENT.md](../reference/REQUIREMENTS-ALIGNMENT.md).
+- **Sync:** POST to `/api/v1/sync/directory`, `/api/v1/sync/order`, etc.; DELETE for sync delete.
+- **Pull:** GET `/api/v1/directory`, `/api/v1/orders`, `/api/v1/evidence`, `/api/v1/credentials`, `/api/v1/reports` (with `since`, `limit`, optional filters).
+- **Auth:** Header `X-API-Key` or query `api_key`.
+- **Response:** JSON; when `conflict: true`, `remoteData` is base64-encoded JSON.
+
+## Implementation checklist
+
+- [ ] Create Xcode project (Swift/SwiftUI or cross-platform); minimum deployment target iOS 15.0.
+- [ ] Store API key in **Keychain**.
+- [ ] Implement **PullAPI** (URLSession or Alamofire): GET endpoints above.
+- [ ] Implement **SyncAPI**: POST sync + DELETE; parse `SyncResponse`, decode `remoteData` when conflict.
+- [ ] **Offline queue:** Queue sync when offline; retry when online; optional Core Data / SwiftData for persistence.
+- [ ] Optional: Face ID / Touch ID for app unlock; certificate pinning for API.
+
+## Discovery
+
+- GET `/api/v1/info` returns `endpoints` (sync, delete, pull) and `auth` for client discovery.
+
+## References
+
+- Backend: [backend/README.md](../../backend/README.md)
+- Platform requirements: [docs/reference/PLATFORM-REQUIREMENTS.md](../reference/PLATFORM-REQUIREMENTS.md)
diff --git a/docs/reference/ANDROID-16-TARGET.md b/docs/reference/ANDROID-16-TARGET.md
new file mode 100644
index 0000000..06d186f
--- /dev/null
+++ b/docs/reference/ANDROID-16-TARGET.md
@@ -0,0 +1,10 @@
+# Android 16 target (compileSdk / targetSdk 36)
+
+- **Android 16** uses **API level 36**. The app currently uses **compileSdk 34** and **targetSdk 34** and runs on Android 16 via compatibility behavior.
+- **To fully target Android 16** (opt into new behavior and APIs):
+ 1. Upgrade **Android Gradle Plugin** to **8.5 or 8.6+** (supports compileSdk 35/36). Update root `build.gradle.kts`: e.g. `id("com.android.application") version "8.6.0"`.
+ 2. In **buildSrc/.../AppConfig.kt**, set `compileSdk = 36` and `targetSdk = 36`.
+ 3. Sync and fix any deprecations or API changes.
+ 4. Test on a device or emulator with Android 16 (API 36).
+
+Until then, **minSdk 24** and **targetSdk 34** remain; the app is forward compatible on Android 16.
diff --git a/docs/reference/DEVICE-COMPATIBILITY.md b/docs/reference/DEVICE-COMPATIBILITY.md
new file mode 100644
index 0000000..05b8ce2
--- /dev/null
+++ b/docs/reference/DEVICE-COMPATIBILITY.md
@@ -0,0 +1,110 @@
+# Device compatibility – Samsung Galaxy Z Fold5 (primary target)
+
+This document describes SMOA compatibility with the **Samsung Galaxy Z Fold5** (model **SM-F946U1**) as the primary target device, and what has been done to ensure the app works correctly on it.
+
+---
+
+## Required target (mandatory minimum)
+
+| Aspect | Required minimum |
+|--------|-------------------|
+| **Device** | Samsung Galaxy Z Fold5 (SM-F946U1) or equivalent (foldable, 4G/5G capable). |
+| **OS** | Android 10 (API 29) or higher; primary target Android 16 (API 36). |
+| **App SDK** | `minSdk 24`, `targetSdk 34` (forward compatible on Android 16). |
+| **Network** | Cellular (4G LTE or 5G NR) and/or Wi‑Fi; optional dual SIM. |
+| **Permissions** | INTERNET, ACCESS_NETWORK_STATE; RECORD_AUDIO, CAMERA for meetings; READ_BASIC_PHONE_STATE optional for 5G MW detection. |
+
+Below minSdk 24 the app does not build. For full Android 16 behavior and testing, targetSdk 36 is recommended once the project upgrades the Android Gradle Plugin.
+
+---
+
+## Target device summary
+
+| Attribute | Value |
+|-----------|--------|
+| **Device** | Samsung Galaxy Z Fold5 (SM-F946U1) |
+| **OS** | Android 16, One UI 8.0 |
+| **Cellular** | 4G LTE, 5G NR, 5G millimeter wave (5G MW) capable |
+| **Connectivity** | Dual SIM (physical + eSIM), e.g. Dark Star + US Mobile |
+| **Security** | SE for Android (Enforcing), Knox 3.12, DualDAR 1.8.0 |
+| **Form factor** | Foldable (cover screen + inner large screen) |
+
+## App compatibility measures
+
+### 1. SDK and API level
+
+- **Current:** `compileSdk = 34`, `targetSdk = 34`, `minSdk = 24` (see `buildSrc/.../AppConfig.kt`).
+- **Android 16** uses **API level 36**. The app is **forward compatible**: it runs on Android 16 with existing targetSdk 34; the system applies compatibility behavior.
+- **Recommendation for full Android 16 optimization:** When upgrading the project’s Android Gradle Plugin (e.g. to 8.9+), set `compileSdk = 36` and `targetSdk = 36` and test against Android 16.
+
+### 2. Foldable support
+
+- **FoldableStateManager** (`core/common`) tracks folded vs unfolded state using a 600 dp width threshold, suitable for Z Fold5 (narrow cover vs wide inner screen).
+- **MainActivity** calls `foldableStateManager.updateFoldState(configuration)` in `onCreate` and **onConfigurationChanged**, so fold/unfold updates the UI without requiring an activity recreate when combined with manifest `configChanges`.
+- **Manifest:** `MainActivity` declares
+ `android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"`
+ so that fold/unfold and size changes are delivered to `onConfigurationChanged` and the activity is not recreated unnecessarily.
+- **MainScreen** receives `foldableStateManager` and can adapt layout (e.g. list/detail, panels) for folded vs unfolded.
+- **PolicyManager** supports a “lock on fold” option for security when the device is folded.
+
+### 3. 4G LTE, 5G, and 5G MW (smart routing)
+
+- **ConnectivityManager** (`core/common`):
+ - **getActiveTransportType()** – WIFI, CELLULAR, VPN, ETHERNET, UNKNOWN.
+ - **getCellularGeneration()** – when transport is CELLULAR, returns LTE_4G, NR_5G, or NR_5G_MW.
+- **Cellular generation logic:**
+ - LTE → `LTE_4G`.
+ - NR (5G) + `TelephonyDisplayInfo.overrideNetworkType == OVERRIDE_NETWORK_TYPE_NR_ADVANCED` (value 5) → **NR_5G_MW** (millimeter wave); otherwise → **NR_5G**.
+- **Permissions:** `READ_BASIC_PHONE_STATE` is declared (optional) to improve accuracy of 4G/5G/5G MW detection on API 29+. Not required for basic connectivity.
+- **Smart routing** (e.g. `MediaRoutingPolicy`, `NetworkPathSelector`) uses transport type and cellular generation to prefer 5G / 5G MW over 4G where appropriate.
+
+### 4. Dual SIM / multi-carrier
+
+- The app uses the system’s **default data network** and **active network capabilities** via `ConnectivityManager` and `NetworkCapabilities`. It does not bind to a specific subscription ID.
+- On dual-SIM devices (e.g. physical SIM + eSIM), the system chooses the active data subscription; SMOA’s connectivity and cellular generation logic apply to whichever subscription is currently used for data. No code changes are required for dual SIM per se.
+
+### 5. Permissions (manifest)
+
+- **Network:** INTERNET, ACCESS_NETWORK_STATE.
+- **Phone state (optional):** READ_BASIC_PHONE_STATE (for 4G/5G/5G MW detection).
+- **Communications:** RECORD_AUDIO, MODIFY_AUDIO_SETTINGS, CAMERA (meetings).
+- **Security:** USE_BIOMETRIC, USE_FINGERPRINT, BIND_VPN_SERVICE.
+- **Storage:** READ/WRITE_EXTERNAL_STORAGE with `maxSdkVersion="32"` where applicable.
+
+### 6. Knox and SE Android
+
+- The app does not use Knox APIs. It runs as a normal Android app; Knox/SE for Android enforce system policy (e.g. device attestation, MDM) independently.
+- If future versions need Knox integration (e.g. Knox SDK for secure storage or VPN), the same device and OS support the required Knox API level (e.g. 39).
+
+## Testing on Z Fold5
+
+- **Fold/unfold:** Open app on cover screen, unfold and fold; confirm layout updates and no unnecessary activity restarts.
+- **Network:** Switch between Wi‑Fi and cellular; on cellular, confirm 4G vs 5G (and 5G+ where available) is reflected if you surface cellular generation in UI or logs.
+- **Dual SIM:** Use one SIM for data, then switch default data to the other; confirm connectivity and routing still work.
+- **Meetings/WebRTC:** Verify camera, microphone, and smart routing (e.g. path selection, codec selection) on both Wi‑Fi and 5G.
+
+---
+
+## Aspects which scale (client / device)
+
+These dimensions scale with usage, device variety, or backend load. The required target above is the floor.
+
+| Aspect | What it scales with | How it scales |
+|--------|---------------------|----------------|
+| **API level (minSdk / targetSdk)** | Newer devices, Android 16+ features | Raise minSdk/targetSdk when dropping older OS support; use `Build.VERSION.SDK_INT` checks for optional APIs (e.g. 5G MW on API 31+). |
+| **Screen size / density** | Folded vs unfolded, different devices | `FoldableStateManager` (600 dp threshold); responsive layouts; `configChanges` so fold/unfold doesn’t recreate Activity. |
+| **Network type** | Wi‑Fi vs 4G vs 5G vs 5G MW | `ConnectivityManager.getActiveTransportType()` and `getCellularGeneration()`; smart routing and adaptive codecs use these. |
+| **Concurrent backend load** | Number of devices syncing / pulling | Backend scales (see [PROXMOX-VE-TEMPLATE-REQUIREMENTS.md](../infrastructure/PROXMOX-VE-TEMPLATE-REQUIREMENTS.md)); app uses rate limit and retries. |
+| **WebRTC sessions** | Number of participants, video quality | Adaptive codec policy and connection-quality tier; TURN/signaling and backend infra scale with sessions. |
+| **Sync volume** | Directory/orders/evidence/reports per device | Backend disk and DB; app queues and syncs by type; no fixed device-side limit. |
+| **Dual SIM / multi-carrier** | Multiple subscriptions | App uses default data network; no per-SIM logic; scales to any number of SIMs as chosen by system. |
+| **Permissions** | Features used (meetings, 5G detection) | Optional permissions (e.g. READ_BASIC_PHONE_STATE) scale with feature set; core works without them. |
+
+---
+
+## References
+
+- **Smart routing / QoS:** [SMART-ROUTING-AND-QOS.md](SMART-ROUTING-AND-QOS.md)
+- **Media codecs (P2M, adaptive):** [MEDIA-CODECS-AND-P2M.md](MEDIA-CODECS-AND-P2M.md)
+- **Backend sync:** `backend/README.md`, `backend/docs/BACKEND-GAPS-AND-ROADMAP.md`
+- **Backend/infra scaling:** [PROXMOX-VE-TEMPLATE-REQUIREMENTS.md](../infrastructure/PROXMOX-VE-TEMPLATE-REQUIREMENTS.md)
diff --git a/docs/reference/MEDIA-CODECS-AND-P2M.md b/docs/reference/MEDIA-CODECS-AND-P2M.md
new file mode 100644
index 0000000..5f3857a
--- /dev/null
+++ b/docs/reference/MEDIA-CODECS-AND-P2M.md
@@ -0,0 +1,49 @@
+# Connection-Speed-Aware Media and Point-to-Multipoint
+
+## Overview
+
+SMOA audio and video (Communications and Meetings modules) use **connection-speed-aware compression codecs** so that encoding adapts to available bandwidth, RTT, and packet loss. This is especially important for **point-to-multipoint** (one sender, many receivers), where different participants may have different link quality.
+
+## Components
+
+| Component | Location | Purpose |
+|-----------|----------|---------|
+| **ConnectionTier** | `communications/domain/AdaptiveCodecPolicy.kt` | Bandwidth tier (VERY_LOW … VERY_HIGH) for codec selection. |
+| **AudioCodecConstraints** | Same | Opus codec limits: min/max bitrate, bandwidth mode (narrowband/wideband/fullband), DTX. |
+| **VideoCodecConstraints** | Same | Video codec (VP8/VP9/H264), max resolution, max bitrate, simulcast/SVC options. |
+| **MediaCodecPolicy** | Same | Maps each ConnectionTier to audio and video constraints; default policy is built-in. |
+| **ConnectionQualityMonitor** | `communications/domain/ConnectionQualityMonitor.kt` | Interface for current quality (bandwidth, RTT, loss, tier). |
+| **StubConnectionQualityMonitor** | `communications/domain/StubConnectionQualityMonitor.kt` | Stub implementation (fixed MEDIUM until WebRTC stats are wired). |
+| **AdaptiveCodecSelector** | `communications/domain/AdaptiveCodecSelector.kt` | Selects current audio/video constraints from policy and quality monitor. |
+| **WebRTCConfig / RTCConfiguration** | `communications/domain/WebRTCConfig.kt`, `WebRTCManager.kt` | Optional media policy; RTC config carries selected audio/video constraints into peer connection setup. |
+
+## Connection Tiers and Default Policy
+
+- **VERY_LOW** (e.g. < 100 kbps): Audio-only or minimal video; Opus narrowband, low bitrate.
+- **LOW** (e.g. 100–256 kbps): Low-resolution video (e.g. 320×240), VP8, constrained audio.
+- **MEDIUM** (e.g. 256–512 kbps): Moderate video (e.g. 640×360), VP8, wideband Opus.
+- **HIGH** (e.g. 512 kbps–1 Mbps): Higher resolution (e.g. 720p), VP8, simulcast (2 layers), fullband Opus.
+- **VERY_HIGH** (e.g. > 1 Mbps): 1080p, VP9, simulcast (3 layers), SVC preferred, fullband Opus.
+
+Exact thresholds are in `connectionTierFromBandwidth()` in `ConnectionQualityMonitor.kt`.
+
+## Point-to-Multipoint
+
+- **Sender**: Uses `AdaptiveCodecSelector.getSendConstraints()` (or current tier) so the **single send** stream uses codec and bitrate appropriate for the current connection. For HIGH/VERY_HIGH, the policy enables **simulcast** (multiple resolution/bitrate layers) so an SFU or receivers can choose the best layer per participant.
+- **Receivers**: When WebRTC stats are integrated, each receiver can use its own `ConnectionQualityMonitor` (or stats) to request the appropriate simulcast layer or SVC spatial/temporal layer from the server.
+- **Stub**: Until WebRTC is fully integrated, `StubConnectionQualityMonitor` reports a fixed MEDIUM tier. Replace with an implementation that parses `RTCStatsReport` (e.g. outbound-rtp, remote-inbound-rtp, candidate-pair) and calls `update(estimatedBandwidthKbps, rttMs, packetLoss)` (or updates a tier) so the selector adapts in real time.
+
+## Applying Constraints When WebRTC Is Integrated
+
+When the WebRTC library is integrated:
+
+1. When creating the peer connection, read `RTCConfiguration.audioConstraints` and `videoConstraints` (already set by `WebRTCManager` from `AdaptiveCodecSelector`).
+2. For **audio**: create the audio track/sender with Opus and apply `minBitrateBps`/`maxBitrateBps` and bandwidth mode (narrowband/wideband/fullband) and DTX from `AudioCodecConstraints`.
+3. For **video**: create the video track/sender with the requested codec (VP8/VP9/H264), cap resolution to `maxWidth`×`maxHeight`, set `maxBitrateBps`; if `useSimulcast` is true, configure the appropriate number of simulcast layers.
+4. Periodically (e.g. from `getStats()` callback), compute estimated bandwidth (and optionally RTT/loss), call `StubConnectionQualityMonitor.update()` or the real monitor’s update, and optionally call `AdaptiveCodecSelector.selectForBandwidth()` so constraints are updated for the next negotiation or track reconfiguration.
+
+## Related
+
+- Communications module: `modules/communications/`
+- Meetings (video transport): `modules/meetings/domain/VideoTransport.kt`
+- WebRTC config: `WebRTCConfig.kt`, `WebRTCManager.kt`
diff --git a/docs/reference/PLATFORM-REQUIREMENTS.md b/docs/reference/PLATFORM-REQUIREMENTS.md
new file mode 100644
index 0000000..91854c0
--- /dev/null
+++ b/docs/reference/PLATFORM-REQUIREMENTS.md
@@ -0,0 +1,101 @@
+# SMOA platform requirements – Android, iOS, Web
+
+This document defines **required targets** and **supported platforms** for SMOA: **Android** (primary), **iOS** (last three generations), and **Web Dapp** (Desktop/Laptop including touch). All platforms use the same backend API contract.
+
+---
+
+## 1. Required target (all platforms)
+
+| Aspect | Required minimum |
+|--------|-------------------|
+| **Backend API** | REST `/api/v1` (sync, pull, delete); JSON request/response; optional X-API-Key auth; CORS for web. |
+| **Sync contract** | POST sync (directory, order, evidence, credential, report); DELETE for sync delete; GET for pull; `SyncResponse` with success, itemId, serverTimestamp, conflict, remoteData (base64 when conflict). |
+| **Auth** | API key via header `X-API-Key` or query `api_key`; when key is set, all `/api/v1/*` require it. |
+| **Network** | HTTPS in production; same-origin or configured CORS for web. |
+
+---
+
+## 2. Android (primary)
+
+| Aspect | Required / supported |
+|--------|----------------------|
+| **OS** | Android 10 (API 29) or higher; primary device Android 16 (API 36). |
+| **App SDK** | minSdk 24, targetSdk 34 (forward compatible on 16). |
+| **Device** | Primary: Samsung Galaxy Z Fold5 (SM-F946U1) or equivalent foldable with 4G/5G. |
+| **Features** | Sync (push/pull/delete), foldable UI, 4G/5G/5G MW detection, WebRTC-ready, VPN-aware routing, biometric. |
+| **Details** | See [DEVICE-COMPATIBILITY.md](DEVICE-COMPATIBILITY.md). |
+
+---
+
+## 3. iOS (last three generations)
+
+SMOA supports **iOS clients** for the same backend; an iOS app is a separate codebase (e.g. Swift/SwiftUI or shared logic via KMP).
+
+| Aspect | Required / supported |
+|--------|----------------------|
+| **OS** | **iOS 15, iOS 16, iOS 17** (last three major generations). Minimum deployment target: **iOS 15.0**. |
+| **Devices** | iPhone and iPad: models that run iOS 15+ (e.g. iPhone 6s and later, iPad Air 2 and later, and subsequent generations). |
+| **Auth** | Same as backend: `X-API-Key` header or `api_key` query; store key in Keychain. |
+| **Sync** | Same REST contract: POST to `/api/v1/sync/*`, DELETE to `/api/v1/sync/{resource}/{id}`, GET to `/api/v1/directory`, `/api/v1/orders`, etc. |
+| **Data** | Decode `SyncResponse.remoteData` as base64 when `conflict == true`; use same DTO field names as backend. |
+| **Networking** | URLSession or Alamofire; certificate pinning optional; respect rate limit (429). |
+| **Offline** | Queue sync when offline; retry when online; optional local persistence (Core Data / SwiftData). |
+| **Touch** | Native touch; support pointer events where applicable (iPad). |
+| **Gaps to implement** | iOS app project (Swift/SwiftUI or cross-platform); Keychain for API key; optional Face ID / Touch ID for app unlock. |
+
+---
+
+## 4. Web Dapp (Desktop / Laptop, including touch)
+
+SMOA supports a **browser-based Web Dapp** for Desktop and Laptop, including **touch devices** (e.g. touch laptops, tablets in browser).
+
+| Aspect | Required / supported |
+|--------|----------------------|
+| **Browsers** | Chrome, Firefox, Safari, Edge (current versions); Desktop and Laptop. |
+| **Viewports** | Responsive layout: desktop (e.g. 1280px+), laptop (1024px+), and tablet/touch (768px+). |
+| **Input** | Mouse + keyboard; **touch** (touchstart/touchend/pointer events) for touch laptops and tablets. |
+| **Auth** | Same backend: `X-API-Key` header or `api_key` query; store in secure storage (e.g. sessionStorage for session, or secure cookie if served from same origin). |
+| **Sync** | Same REST contract; use `fetch` or axios; CORS must allow the web origin (backend `smoa.cors.allowed-origins`). |
+| **Data** | Same JSON DTOs; decode `remoteData` base64 when `conflict == true`. |
+| **Offline** | Optional: Service Worker + Cache API; queue sync in IndexedDB/localStorage and flush when online. |
+| **HTTPS** | Required in production; backend behind TLS; web app served over HTTPS. |
+| **PWA (optional)** | Installable; optional offline shell; same API contract. |
+| **Gaps to implement** | Web app codebase (e.g. React, Vue, Svelte); build and host; configure CORS for web origin. |
+
+---
+
+## 5. Backend support for all clients
+
+The backend **already supports** Android, iOS, and Web:
+
+| Feature | Backend | Android | iOS | Web |
+|---------|---------|---------|-----|-----|
+| **Sync POST** | ✅ | ✅ | Same contract | Same contract |
+| **Sync DELETE** | ✅ | ✅ | Same contract | Same contract |
+| **Pull GET** | ✅ | ✅ | Same contract | Same contract |
+| **API key auth** | ✅ | ✅ | Same contract | Same contract |
+| **CORS** | ✅ configurable | N/A | N/A | ✅ use allowed-origins |
+| **Rate limit** | ✅ per key/IP | ✅ | Same | Same |
+| **Health / info** | ✅ GET /health, GET /api/v1/info | ✅ | Same | Same |
+
+- **CORS:** Set `smoa.cors.allowed-origins` to the web app origin(s) (e.g. `https://smoa.example.com`) when deploying the Web Dapp; use `*` only for dev if acceptable.
+- **Discovery:** GET `/api/v1/info` returns endpoint list so any client (Android, iOS, Web) can discover sync, delete, and pull URLs.
+
+---
+
+## 6. Scaling (all platforms)
+
+| Aspect | Scales with | Notes |
+|--------|-------------|--------|
+| **Concurrent devices** | Number of Android + iOS + Web clients | Backend rate limit and VM sizing; see [PROXMOX-VE-TEMPLATE-REQUIREMENTS.md](../infrastructure/PROXMOX-VE-TEMPLATE-REQUIREMENTS.md). |
+| **Sync volume** | Entities per user, pull page size | Backend DB and disk; clients use since/limit on GET. |
+| **Web origins** | Multiple Dapp domains | Add all origins to `smoa.cors.allowed-origins` (comma-separated). |
+
+---
+
+## 7. References
+
+- [DEVICE-COMPATIBILITY.md](DEVICE-COMPATIBILITY.md) – Android device (Z Fold5) and app
+- [REQUIREMENTS-ALIGNMENT.md](REQUIREMENTS-ALIGNMENT.md) – Frontend–backend contract and gaps
+- [BACKEND-GAPS-AND-ROADMAP.md](../../backend/docs/BACKEND-GAPS-AND-ROADMAP.md) – Backend API and ops
+- [PROXMOX-VE-TEMPLATE-REQUIREMENTS.md](../infrastructure/PROXMOX-VE-TEMPLATE-REQUIREMENTS.md) – Infra sizing
diff --git a/docs/reference/REQUIREMENTS-ALIGNMENT.md b/docs/reference/REQUIREMENTS-ALIGNMENT.md
new file mode 100644
index 0000000..ccc6109
--- /dev/null
+++ b/docs/reference/REQUIREMENTS-ALIGNMENT.md
@@ -0,0 +1,103 @@
+# SMOA requirements alignment – frontend and backend
+
+This document maps **requirements** between the **device application** (Android; future iOS and Web) and the **backend**, and lists **gaps** with ownership (device vs backend).
+
+---
+
+## 1. Sync contract (frontend ↔ backend)
+
+All clients (Android, iOS, Web) use the same REST contract.
+
+| Requirement | Backend | Android app | iOS (to build) | Web Dapp (to build) |
+|-------------|---------|-------------|-----------------|----------------------|
+| **POST sync** (directory, order, evidence, credential, report) | ✅ SyncController | ✅ SyncAPI + SyncService | Same contract | Same contract |
+| **SyncResponse** (success, itemId, serverTimestamp, conflict, remoteData, message) | ✅ | ✅ core/common SyncResponse | Same | Same |
+| **Conflict** (server returns conflict + base64 remoteData) | ✅ | ✅ SyncService handles ConflictException | Same | Same |
+| **DELETE** (sync delete) | ✅ | ✅ SyncAPI.delete* + SyncService on SyncOperation.Delete | Same | Same |
+| **Pull GET** (directory, orders, evidence, credentials, reports) | ✅ PullController | ✅ Use GET with since/limit | Same | Same |
+| **Auth** (X-API-Key or api_key) | ✅ | ✅ Send header/query when configured | Same | Same |
+| **Rate limit** (429, configurable RPM) | ✅ | ✅ Retry with backoff | Same | Same |
+
+---
+
+## 2. API surface (backend)
+
+| Endpoint | Method | Purpose |
+|----------|--------|---------|
+| `/health` | GET | Liveness; db status. |
+| `/api/v1/info` | GET | Discovery: name, version, list of sync/pull/delete endpoints. |
+| `/api/v1/sync/directory` | POST | Sync directory entry. |
+| `/api/v1/sync/order` | POST | Sync order. |
+| `/api/v1/sync/evidence` | POST | Sync evidence. |
+| `/api/v1/sync/credential` | POST | Sync credential. |
+| `/api/v1/sync/report` | POST | Sync report. |
+| `/api/v1/sync/directory/{id}` | DELETE | Delete directory entry. |
+| `/api/v1/sync/order/{orderId}` | DELETE | Delete order. |
+| `/api/v1/sync/evidence/{evidenceId}` | DELETE | Delete evidence. |
+| `/api/v1/sync/credential/{credentialId}` | DELETE | Delete credential. |
+| `/api/v1/sync/report/{reportId}` | DELETE | Delete report. |
+| `/api/v1/directory` | GET | List directory (optional unit, X-Unit). |
+| `/api/v1/orders` | GET | List orders (since, limit, jurisdiction / X-Unit). |
+| `/api/v1/evidence` | GET | List evidence (since, limit, caseNumber). |
+| `/api/v1/credentials` | GET | List credentials (since, limit, holderId). |
+| `/api/v1/reports` | GET | List reports (since, limit). |
+
+---
+
+## 3. DTO alignment (device → backend)
+
+Device sends JSON that matches backend request DTOs; backend returns JSON that matches device expectations.
+
+| Resource | Request (device → backend) | Response (backend → device) |
+|----------|----------------------------|-----------------------------|
+| Directory | DirectorySyncRequest (id, name, title, unit, …; lastUpdated) | SyncResponse |
+| Order | OrderSyncRequest (orderId, orderType, title, content, …; clientUpdatedAt) | SyncResponse |
+| Evidence | EvidenceSyncRequest (evidenceId, caseNumber, …; clientUpdatedAt) | SyncResponse |
+| Credential | CredentialSyncRequest (credentialId, holderId, …; clientUpdatedAt) | SyncResponse |
+| Report | ReportSyncRequest (reportId, reportType, title, format, …; clientUpdatedAt) | SyncResponse |
+
+**Enums (validation):** orderType, status, evidenceType, reportType, format — backend uses `@Pattern`; device must send allowed values (see backend SyncRequest.kt).
+
+---
+
+## 4. Gaps and ownership
+
+### 4.1 Filled by device (Android)
+
+| Gap | Status | Notes |
+|-----|--------|--------|
+| **Real SyncAPI implementation** | ✅ Done | BackendSyncAPI (app) calls backend when BuildConfig.SMOA_BACKEND_BASE_URL set; build with -Psmoa.backend.baseUrl=http://host:8080. |
+| **SyncService uses SyncAPI** | ✅ Done | CommonModule provides SyncService(syncAPI); AppModule provides SyncAPI (BackendSyncAPI or DefaultSyncAPI). |
+| **Delete operation** | ✅ Done | SyncService calls syncAPI.delete*(item.id) when item.operation == SyncOperation.Delete. |
+| **Pull on connect** | Optional | On connectivity restored, call GET endpoints and merge into local DB. |
+
+### 4.2 Filled by backend
+
+| Gap | Status | Notes |
+|-----|--------|--------|
+| **CORS for Web** | ✅ | smoa.cors.allowed-origins; set to web app origin(s) for production. |
+| **Info endpoint** | ✅ | GET /api/v1/info lists all sync, delete, and pull endpoints for client discovery. |
+| **Auth for all clients** | ✅ | API key required when smoa.api.key set; same for Android, iOS, Web. |
+
+### 4.3 Filled by iOS (when built)
+
+| Gap | Owner | Notes |
+|-----|--------|--------|
+| **iOS app** | iOS | Swift/SwiftUI or KMP; same REST contract; Keychain for API key. |
+| **Offline queue** | iOS | Queue sync when offline; retry when online. |
+
+### 4.4 Filled by Web Dapp (when built)
+
+| Gap | Owner | Notes |
+|-----|--------|--------|
+| **Web app** | Web | SPA (e.g. React/Vue); responsive + touch; same REST contract. |
+| **CORS origin** | Backend config | Set smoa.cors.allowed-origins to web origin. |
+| **Secure storage** | Web | sessionStorage or secure cookie for API key/session. |
+
+---
+
+## 5. References
+
+- **Backend API:** `backend/README.md`, OpenAPI `/v3/api-docs`, `/swagger-ui.html`
+- **Mobile contract:** `core/common/SyncAPI.kt`, `SyncService.kt`
+- **Platforms:** [PLATFORM-REQUIREMENTS.md](PLATFORM-REQUIREMENTS.md)
diff --git a/docs/reference/SMART-ROUTING-AND-QOS.md b/docs/reference/SMART-ROUTING-AND-QOS.md
new file mode 100644
index 0000000..4b4b40a
--- /dev/null
+++ b/docs/reference/SMART-ROUTING-AND-QOS.md
@@ -0,0 +1,68 @@
+# Smart Routing, QoS, Lag Reduction, and System Stability
+
+## Overview
+
+SMOA implements **smart routing** and **QoS (Quality of Service)** for media (voice/video) to improve quality, reduce lag, manage infrastructure, and keep the system stable under poor conditions.
+
+## Components
+
+### Core (core/common)
+
+| Component | Purpose |
+|-----------|---------|
+| **CircuitBreaker** | Per-endpoint failure handling: after N failures the circuit opens and calls fail fast until reset timeout. Used by InfrastructureManager for STUN/TURN/signaling. |
+| **QoSPolicy / TrafficClass** | Traffic classification (VOICE, VIDEO, SIGNALING, DATA) and priority; policy caps (max concurrent sessions, max total send bitrate) for stability. |
+| **ConnectivityManager** | Extended with `getActiveTransportType()` (WIFI, CELLULAR, VPN, ETHERNET) and `getCellularGeneration()` (4G LTE, 5G, 5G MW) for path selection. |
+| **NetworkTransportType** | Enum for transport used by routing policy. |
+| **CellularGeneration** | When on cellular: LTE_4G, NR_5G, NR_5G_MW (millimeter wave), UNKNOWN. Used to prefer 5G / 5G MW over 4G. |
+
+### Communications (modules/communications)
+
+| Component | Purpose |
+|-----------|---------|
+| **MediaRoutingPolicy** | Path preference: prefer low latency, prefer VPN when required, transport order, path failover, min bandwidth for video. |
+| **NetworkPathSelector** | Selects best network path for media using ConnectivityManager, VPNManager, and MediaRoutingPolicy; exposes `SelectedPath` (transport, cellularGeneration when CELLULAR, recommendedForVideo). On cellular, ranks 4G LTE, 5G, and 5G MW per policy. |
+| **InfrastructureManager** | Manages STUN/TURN/signaling endpoint lists; uses CircuitBreaker for health; `getHealthyStunUrls()`, `getHealthyTurnServers()`, `getHealthySignalingUrl()`; `buildWebRTCConfig()` for WebRTC with failover. |
+| **ConnectionStabilityController** | Reconnection exponential backoff; degradation mode (NONE, AUDIO_ONLY, REDUCED_VIDEO); session count and bitrate caps from QoSPolicy. |
+| **SmartRoutingService** | Orchestrates path selection, infra, stability, and adaptive codecs; exposes `RoutingState`, `getWebRTCConfig()`, `tryStartSession()`, `recordConnectionSuccess/Failure`, `updateFromConnectionQuality()`, `onConnectivityChanged()`. |
+
+## QoS and Lag Reduction
+
+- **Traffic classes**: Voice (highest), Video, Signaling, Data. Used for scheduling and prioritization hints.
+- **Path selection**: Prefer Wi-Fi/VPN over cellular when policy says so; when on cellular, prefer 5G MW > 5G > 4G LTE (configurable via `cellularGenerationPreferenceOrder`). Avoid sending video when path is not recommended.
+- **Adaptive codecs**: Connection-speed-aware codecs (see [MEDIA-CODECS-AND-P2M.md](MEDIA-CODECS-AND-P2M.md)) reduce bitrate on slow links, reducing buffering and lag.
+- **Reconnection backoff**: Exponential backoff after connection failures to avoid hammering endpoints and reduce perceived instability.
+- **Graceful degradation**: When connection tier is VERY_LOW (or policy says so), switch to AUDIO_ONLY to preserve voice and reduce load.
+
+## Infrastructure Management
+
+- **STUN/TURN/signaling**: Configure via `InfrastructureManager.setStunEndpoints()`, `setTurnEndpoints()`, `setSignalingEndpoints()`.
+- **Health**: Each endpoint is protected by a circuit breaker; after a threshold of failures the endpoint is skipped until reset timeout.
+- **Failover**: `getHealthyStunUrls()` / `getHealthyTurnServers()` / `getHealthySignalingUrl()` return only endpoints with closed circuits; WebRTC config is built from these for automatic failover.
+
+## System Stability
+
+- **Session cap**: `QoSPolicy.maxConcurrentSessions` limits concurrent media sessions; `SmartRoutingService.tryStartSession()` enforces it.
+- **Bitrate cap**: `QoSPolicy.maxTotalSendBitrateBps` can be enforced by the app when sending (ConnectionStabilityController.isWithinBitrateCap()).
+- **Circuit breakers**: Prevent cascading failures to unhealthy STUN/TURN/signaling servers.
+- **Degradation**: AUDIO_ONLY and REDUCED_VIDEO reduce load when quality is poor.
+
+## Integration
+
+- **WebRTCManager**: Uses `SmartRoutingService.getWebRTCConfig()` for ICE/signaling config (healthy infra) and adaptive codec constraints.
+- **VideoTransport** (meetings): Uses `SmartRoutingService.tryStartSession()` / `notifySessionEnded()`, `getRoutingState().recommendedForVideo` to decide audio-only vs video, and `recordConnectionSuccess/Failure()` for backoff.
+- **Connectivity changes**: Call `SmartRoutingService.onConnectivityChanged()` when connectivity or VPN state changes so path selection and routing state are updated.
+- **Quality updates**: When WebRTC stats (or network callback) provide new bandwidth/RTT/loss, update the connection quality monitor and call `SmartRoutingService.updateFromConnectionQuality()` to adapt codecs and degradation.
+
+## Configuration
+
+- **MediaRoutingPolicy**: Default prefers low latency and VPN when required; customize transport order, `cellularGenerationPreferenceOrder` (4G LTE, 5G, 5G MW), and `minBandwidthKbpsForVideo` per deployment. Cellular generation is derived from `TelephonyManager` (API 29+ for 5G NR; API 31+ for 5G MW when `OVERRIDE_NETWORK_TYPE_NR_ADVANCED` is reported).
+- **QoSPolicy**: Set via `SmartRoutingService.setQoSPolicy()` (session cap, bitrate cap).
+- **Circuit breaker**: Threshold and reset timeout are in InfrastructureManager (e.g. 3 failures, 60s reset); adjust as needed.
+- **StabilityController**: `minBackoffMs`, `maxBackoffMs`, `backoffMultiplier` control reconnection backoff.
+
+## Related
+
+- [MEDIA-CODECS-AND-P2M.md](MEDIA-CODECS-AND-P2M.md) – Connection-speed-aware audio/video codecs and point-to-multipoint.
+- Communications module: `modules/communications/domain/`.
+- Core common: `core/common/` (CircuitBreaker, QoS, ConnectivityManager).
diff --git a/docs/status/IMPLEMENTATION_STATUS.md b/docs/status/IMPLEMENTATION_STATUS.md
index 173407b..48397ea 100644
--- a/docs/status/IMPLEMENTATION_STATUS.md
+++ b/docs/status/IMPLEMENTATION_STATUS.md
@@ -197,6 +197,16 @@ For detailed compliance information, see:
## Remaining Work
+**See [TODO.md](../../TODO.md)** for the full checklist of remaining and optional tasks (backend, Android, iOS, Web, infrastructure, compliance, testing).
+
+### Next steps (short-term)
+
+1. **Backend:** Run `./gradlew :backend:test` and fix any failures; add integration tests for sync/pull/health.
+2. **Android 16:** When upgrading AGP to 8.5+, set `compileSdk = 36`, `targetSdk = 36` (see [ANDROID-16-TARGET.md](../reference/ANDROID-16-TARGET.md)).
+3. **Web:** Expand [web scaffold](../web-scaffold/index.html) (directory pull and status UI are in place); optional: React/Vue SPA, build pipeline, CORS in production.
+4. **iOS / Web Dapp:** Full apps are separate codebases; use [docs/ios/README.md](../ios/README.md) and web scaffold as starting points.
+5. **Domain/compliance:** NCIC, ATF, eIDAS QTSP, full WebRTC/AS4/signing require external approvals or larger implementations; extend stubs as needed.
+
### High Priority (Future Enhancements)
1. **WebRTC Full Library Integration**
diff --git a/docs/web-scaffold/index.html b/docs/web-scaffold/index.html
new file mode 100644
index 0000000..5504653
--- /dev/null
+++ b/docs/web-scaffold/index.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+ SMOA Web
+
+
+
+ SMOA Web
+ Base URL:
+ API key:
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/communications/README.md b/modules/communications/README.md
new file mode 100644
index 0000000..4ade5ea
--- /dev/null
+++ b/modules/communications/README.md
@@ -0,0 +1,14 @@
+# Communications module
+
+WebRTC-based communications (voice, video, signaling) with infrastructure failover.
+
+## Configurable endpoints
+
+**InfrastructureManager** manages STUN, TURN, and signaling URLs. The **app** configures them at startup from BuildConfig (when set):
+
+- **STUN:** `smoa.stun.urls` – comma-separated (e.g. `stun:stun.l.google.com:19302,stun:stun.example.com:3478`). Passed as `-Psmoa.stun.urls=...` when building the app.
+- **Signaling:** `smoa.signaling.urls` – comma-separated signaling server URLs for failover. Passed as `-Psmoa.signaling.urls=...`.
+
+TURN servers (with optional credentials) are set programmatically via `InfrastructureManager.setTurnEndpoints(List)` where needed.
+
+See **SMOAApplication.configureInfrastructure()** and **app/build.gradle.kts** (BuildConfig fields `SMOA_STUN_URLS`, `SMOA_SIGNALING_URLS`).
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/di/CommunicationsModule.kt b/modules/communications/src/main/java/com/smoa/modules/communications/di/CommunicationsModule.kt
index 17eb1e0..5c5315d 100644
--- a/modules/communications/src/main/java/com/smoa/modules/communications/di/CommunicationsModule.kt
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/di/CommunicationsModule.kt
@@ -2,8 +2,17 @@ package com.smoa.modules.communications.di
import android.content.Context
import com.smoa.core.security.AuditLogger
+import com.smoa.modules.communications.domain.AdaptiveCodecSelector
import com.smoa.modules.communications.domain.ChannelManager
import com.smoa.modules.communications.domain.CommunicationsService
+import com.smoa.modules.communications.domain.ConnectionQualityMonitor
+import com.smoa.modules.communications.domain.ConnectionStabilityController
+import com.smoa.modules.communications.domain.InfrastructureManager
+import com.smoa.modules.communications.domain.MediaCodecPolicy
+import com.smoa.modules.communications.domain.MediaRoutingPolicy
+import com.smoa.modules.communications.domain.NetworkPathSelector
+import com.smoa.modules.communications.domain.SmartRoutingService
+import com.smoa.modules.communications.domain.StubConnectionQualityMonitor
import com.smoa.modules.communications.domain.VoiceTransport
import com.smoa.modules.communications.domain.WebRTCManager
import dagger.Module
@@ -16,12 +25,62 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object CommunicationsModule {
+ @Provides
+ @Singleton
+ fun provideConnectionQualityMonitor(
+ stub: StubConnectionQualityMonitor
+ ): ConnectionQualityMonitor = stub
+
+ @Provides
+ @Singleton
+ fun provideMediaCodecPolicy(): MediaCodecPolicy = MediaCodecPolicy.default()
+
+ @Provides
+ @Singleton
+ fun provideMediaRoutingPolicy(): MediaRoutingPolicy = MediaRoutingPolicy()
+
+ @Provides
+ @Singleton
+ fun provideNetworkPathSelector(
+ connectivityManager: com.smoa.core.common.ConnectivityManager,
+ vpnManager: com.smoa.core.security.VPNManager,
+ policy: MediaRoutingPolicy
+ ): NetworkPathSelector = NetworkPathSelector(connectivityManager, vpnManager, policy)
+
+ @Provides
+ @Singleton
+ fun provideInfrastructureManager(
+ circuitBreaker: com.smoa.core.common.CircuitBreaker
+ ): InfrastructureManager = InfrastructureManager(circuitBreaker)
+
+ @Provides
+ @Singleton
+ fun provideConnectionStabilityController(): ConnectionStabilityController = ConnectionStabilityController()
+
+ @Provides
+ @Singleton
+ fun provideSmartRoutingService(
+ networkPathSelector: NetworkPathSelector,
+ infrastructureManager: InfrastructureManager,
+ stabilityController: ConnectionStabilityController,
+ adaptiveCodecSelector: AdaptiveCodecSelector,
+ connectionQualityMonitor: ConnectionQualityMonitor
+ ): SmartRoutingService = SmartRoutingService(
+ networkPathSelector,
+ infrastructureManager,
+ stabilityController,
+ adaptiveCodecSelector,
+ connectionQualityMonitor
+ )
+
@Provides
@Singleton
fun provideWebRTCManager(
- @ApplicationContext context: Context
+ @ApplicationContext context: Context,
+ adaptiveCodecSelector: AdaptiveCodecSelector,
+ smartRoutingService: SmartRoutingService
): WebRTCManager {
- return WebRTCManager(context)
+ return WebRTCManager(context, adaptiveCodecSelector, smartRoutingService)
}
@Provides
@@ -33,9 +92,10 @@ object CommunicationsModule {
@Provides
@Singleton
fun provideVoiceTransport(
- webRTCManager: WebRTCManager
+ webRTCManager: WebRTCManager,
+ smartRoutingService: SmartRoutingService
): VoiceTransport {
- return VoiceTransport(webRTCManager)
+ return VoiceTransport(webRTCManager, smartRoutingService)
}
@Provides
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/AdaptiveCodecPolicy.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/AdaptiveCodecPolicy.kt
new file mode 100644
index 0000000..8fe39ca
--- /dev/null
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/AdaptiveCodecPolicy.kt
@@ -0,0 +1,137 @@
+package com.smoa.modules.communications.domain
+
+/**
+ * Connection speed tier used to select compression codecs and bitrate limits.
+ * Enables connection-speed-aware audio/video encoding, especially for
+ * point-to-multipoint where one sender serves many receivers at varying link quality.
+ */
+enum class ConnectionTier {
+ VERY_LOW,
+ LOW,
+ MEDIUM,
+ HIGH,
+ VERY_HIGH
+}
+
+/**
+ * Audio codec constraints for connection-speed-aware compression.
+ * Opus is the preferred WebRTC codec; it supports adaptive bitrate and bandwidth modes.
+ */
+data class AudioCodecConstraints(
+ val codec: String = "opus",
+ val minBitrateBps: Int,
+ val maxBitrateBps: Int,
+ val opusBandwidthMode: OpusBandwidthMode = OpusBandwidthMode.WIDEBAND,
+ val useDtx: Boolean = true
+)
+
+enum class OpusBandwidthMode {
+ NARROWBAND,
+ WIDEBAND,
+ FULLBAND
+}
+
+/**
+ * Video codec constraints for connection-speed-aware compression.
+ * VP8/VP9 suit simulcast for point-to-multipoint; VP9 supports SVC.
+ */
+data class VideoCodecConstraints(
+ val codec: String = "VP8",
+ val maxWidth: Int,
+ val maxHeight: Int,
+ val maxBitrateBps: Int,
+ val useSimulcast: Boolean = false,
+ val simulcastLayers: Int = 2,
+ val preferSvc: Boolean = false
+)
+
+/**
+ * Policy mapping connection tier to audio and video codec constraints.
+ * Used by AdaptiveCodecSelector for connection-speed-aware compression.
+ */
+data class MediaCodecPolicy(
+ val audioByTier: Map,
+ val videoByTier: Map
+) {
+ fun audioForTier(tier: ConnectionTier): AudioCodecConstraints =
+ audioByTier[tier] ?: audioByTier[ConnectionTier.MEDIUM]!!
+ fun videoForTier(tier: ConnectionTier): VideoCodecConstraints =
+ videoByTier[tier] ?: videoByTier[ConnectionTier.MEDIUM]!!
+
+ companion object {
+ fun default(): MediaCodecPolicy {
+ val audioByTier = mapOf(
+ ConnectionTier.VERY_LOW to AudioCodecConstraints(
+ minBitrateBps = 12_000,
+ maxBitrateBps = 24_000,
+ opusBandwidthMode = OpusBandwidthMode.NARROWBAND,
+ useDtx = true
+ ),
+ ConnectionTier.LOW to AudioCodecConstraints(
+ minBitrateBps = 24_000,
+ maxBitrateBps = 48_000,
+ opusBandwidthMode = OpusBandwidthMode.WIDEBAND,
+ useDtx = true
+ ),
+ ConnectionTier.MEDIUM to AudioCodecConstraints(
+ minBitrateBps = 32_000,
+ maxBitrateBps = 64_000,
+ opusBandwidthMode = OpusBandwidthMode.WIDEBAND,
+ useDtx = true
+ ),
+ ConnectionTier.HIGH to AudioCodecConstraints(
+ minBitrateBps = 48_000,
+ maxBitrateBps = 128_000,
+ opusBandwidthMode = OpusBandwidthMode.FULLBAND,
+ useDtx = true
+ ),
+ ConnectionTier.VERY_HIGH to AudioCodecConstraints(
+ minBitrateBps = 64_000,
+ maxBitrateBps = 256_000,
+ opusBandwidthMode = OpusBandwidthMode.FULLBAND,
+ useDtx = true
+ )
+ )
+ val videoByTier = mapOf(
+ ConnectionTier.VERY_LOW to VideoCodecConstraints(
+ maxWidth = 0,
+ maxHeight = 0,
+ maxBitrateBps = 0,
+ useSimulcast = false
+ ),
+ ConnectionTier.LOW to VideoCodecConstraints(
+ codec = "VP8",
+ maxWidth = 320,
+ maxHeight = 240,
+ maxBitrateBps = 150_000,
+ useSimulcast = false
+ ),
+ ConnectionTier.MEDIUM to VideoCodecConstraints(
+ codec = "VP8",
+ maxWidth = 640,
+ maxHeight = 360,
+ maxBitrateBps = 400_000,
+ useSimulcast = false
+ ),
+ ConnectionTier.HIGH to VideoCodecConstraints(
+ codec = "VP8",
+ maxWidth = 1280,
+ maxHeight = 720,
+ maxBitrateBps = 1_200_000,
+ useSimulcast = true,
+ simulcastLayers = 2
+ ),
+ ConnectionTier.VERY_HIGH to VideoCodecConstraints(
+ codec = "VP9",
+ maxWidth = 1920,
+ maxHeight = 1080,
+ maxBitrateBps = 2_500_000,
+ useSimulcast = true,
+ simulcastLayers = 3,
+ preferSvc = true
+ )
+ )
+ return MediaCodecPolicy(audioByTier = audioByTier, videoByTier = videoByTier)
+ }
+ }
+}
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/AdaptiveCodecSelector.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/AdaptiveCodecSelector.kt
new file mode 100644
index 0000000..8adbe56
--- /dev/null
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/AdaptiveCodecSelector.kt
@@ -0,0 +1,64 @@
+package com.smoa.modules.communications.domain
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Selects audio and video codec constraints based on observed connection speed.
+ * Uses [ConnectionQualityMonitor] and [MediaCodecPolicy] to choose
+ * connection-speed-aware compression for point-to-point and point-to-multipoint.
+ *
+ * For point-to-multipoint, the sender can use the selected constraints to encode
+ * a single adaptive stream or simulcast layers so that receivers with different
+ * link quality each get an appropriate layer.
+ */
+@Singleton
+class AdaptiveCodecSelector @Inject constructor(
+ private val policy: MediaCodecPolicy,
+ private val qualityMonitor: ConnectionQualityMonitor
+) {
+ private val _audioConstraints = MutableStateFlow(policy.audioForTier(ConnectionTier.MEDIUM))
+ val audioConstraints: StateFlow = _audioConstraints.asStateFlow()
+
+ private val _videoConstraints = MutableStateFlow(policy.videoForTier(ConnectionTier.MEDIUM))
+ val videoConstraints: StateFlow = _videoConstraints.asStateFlow()
+
+ init {
+ // When quality updates, recompute constraints (real impl would collect from qualityMonitor.qualityUpdates())
+ // For now, constraints are updated via selectForTier() when the app has new quality data.
+ }
+
+ /**
+ * Update selected constraints from the current connection tier.
+ * Call when WebRTC stats (or network callback) indicate a tier change.
+ */
+ fun selectForTier(tier: ConnectionTier) {
+ _audioConstraints.value = policy.audioForTier(tier)
+ _videoConstraints.value = policy.videoForTier(tier)
+ }
+
+ /**
+ * Update selected constraints from estimated bandwidth (and optional RTT/loss).
+ * Convenience for callers that have raw stats.
+ */
+ fun selectForBandwidth(estimatedBandwidthKbps: Int, rttMs: Int = -1, packetLoss: Float = -1f) {
+ val tier = connectionTierFromBandwidth(estimatedBandwidthKbps, rttMs, packetLoss)
+ selectForTier(tier)
+ }
+
+ /** Current audio constraints for the active connection tier. */
+ fun getAudioConstraints(): AudioCodecConstraints = _audioConstraints.value
+
+ /** Current video constraints for the active connection tier. */
+ fun getVideoConstraints(): VideoCodecConstraints = _videoConstraints.value
+
+ /**
+ * Get constraints for point-to-multipoint send: use current tier; if policy
+ * enables simulcast for this tier, caller should configure multiple layers.
+ */
+ fun getSendConstraints(): Pair =
+ _audioConstraints.value to _videoConstraints.value
+}
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/ConnectionQualityMonitor.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/ConnectionQualityMonitor.kt
new file mode 100644
index 0000000..05eea96
--- /dev/null
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/ConnectionQualityMonitor.kt
@@ -0,0 +1,51 @@
+package com.smoa.modules.communications.domain
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * Observed connection quality for a peer or session.
+ * Used to drive connection-speed-aware codec selection (audio/video compression).
+ */
+data class ConnectionQuality(
+ /** Estimated available bandwidth in kbps (0 if unknown). */
+ val estimatedBandwidthKbps: Int,
+ /** Round-trip time in ms (-1 if unknown). */
+ val rttMs: Int,
+ /** Packet loss fraction 0.0..1.0 (-1f if unknown). */
+ val packetLossFraction: Float,
+ /** Derived tier for codec selection. */
+ val tier: ConnectionTier
+)
+
+/**
+ * Monitors connection quality (bandwidth, RTT, loss) and exposes a current tier
+ * or quality metrics. Implementations should feed from WebRTC stats (e.g.
+ * RTCStatsReport outbound-rtp, remote-inbound-rtp, candidate-pair) when
+ * the WebRTC stack is integrated.
+ *
+ * Essential for point-to-multipoint: each receiver (or the sender, when
+ * using receiver feedback) can use this to choose appropriate simulcast
+ * layer or SVC spatial/temporal layer.
+ */
+interface ConnectionQualityMonitor {
+ /** Current connection quality; updates when stats are available. */
+ val currentQuality: StateFlow
+ /** Flow of quality updates for reactive codec adaptation. */
+ fun qualityUpdates(): Flow
+}
+
+/**
+ * Derives [ConnectionTier] from estimated bandwidth (and optionally RTT/loss).
+ * Thresholds aligned with [MediaCodecPolicy.default] tiers.
+ */
+fun connectionTierFromBandwidth(estimatedBandwidthKbps: Int, rttMs: Int = -1, packetLoss: Float = -1f): ConnectionTier {
+ return when {
+ estimatedBandwidthKbps <= 0 -> ConnectionTier.MEDIUM
+ estimatedBandwidthKbps < 100 -> ConnectionTier.VERY_LOW
+ estimatedBandwidthKbps < 256 -> ConnectionTier.LOW
+ estimatedBandwidthKbps < 512 -> ConnectionTier.MEDIUM
+ estimatedBandwidthKbps < 1000 -> ConnectionTier.HIGH
+ else -> ConnectionTier.VERY_HIGH
+ }
+}
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/ConnectionStabilityController.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/ConnectionStabilityController.kt
new file mode 100644
index 0000000..dd6a629
--- /dev/null
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/ConnectionStabilityController.kt
@@ -0,0 +1,79 @@
+package com.smoa.modules.communications.domain
+
+import com.smoa.core.common.QoSPolicy
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Controls connection stability: reconnection backoff, graceful degradation, and resource caps.
+ * Reduces lag and improves system stability under poor conditions.
+ */
+@Singleton
+class ConnectionStabilityController @Inject constructor() {
+ private val _reconnectBackoffMs = MutableStateFlow(0L)
+ val reconnectBackoffMs: StateFlow = _reconnectBackoffMs.asStateFlow()
+
+ private val _degradationMode = MutableStateFlow(DegradationMode.NONE)
+ val degradationMode: StateFlow = _degradationMode.asStateFlow()
+
+ private val _activeSessionCount = MutableStateFlow(0)
+ val activeSessionCount: StateFlow = _activeSessionCount.asStateFlow()
+
+ private var consecutiveFailures = 0
+ private var qosPolicy: QoSPolicy = QoSPolicy()
+
+ var minBackoffMs: Long = 1_000L
+ var maxBackoffMs: Long = 60_000L
+ var backoffMultiplier: Double = 2.0
+
+ fun setQoSPolicy(policy: QoSPolicy) {
+ qosPolicy = policy
+ }
+
+ fun recordConnectionFailure(): Long {
+ consecutiveFailures++
+ var backoff = minBackoffMs
+ repeat(consecutiveFailures - 1) {
+ backoff = (backoff * backoffMultiplier).toLong().coerceAtMost(maxBackoffMs)
+ }
+ backoff = backoff.coerceIn(minBackoffMs, maxBackoffMs)
+ _reconnectBackoffMs.value = backoff
+ return backoff
+ }
+
+ fun recordConnectionSuccess() {
+ consecutiveFailures = 0
+ _reconnectBackoffMs.value = 0L
+ }
+
+ fun setDegradationMode(mode: DegradationMode) {
+ _degradationMode.value = mode
+ }
+
+ fun shouldDisableVideo(): Boolean = _degradationMode.value == DegradationMode.AUDIO_ONLY
+
+ fun notifySessionStarted(): Boolean {
+ val max = qosPolicy.maxConcurrentSessions
+ if (max > 0 && _activeSessionCount.value >= max) return false
+ _activeSessionCount.value = _activeSessionCount.value + 1
+ return true
+ }
+
+ fun notifySessionEnded() {
+ _activeSessionCount.value = (_activeSessionCount.value - 1).coerceAtLeast(0)
+ }
+
+ fun isWithinBitrateCap(currentSendBitrateBps: Int): Boolean {
+ val max = qosPolicy.maxTotalSendBitrateBps
+ return max <= 0 || currentSendBitrateBps <= max
+ }
+}
+
+enum class DegradationMode {
+ NONE,
+ AUDIO_ONLY,
+ REDUCED_VIDEO
+}
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/InfrastructureManager.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/InfrastructureManager.kt
new file mode 100644
index 0000000..fcc0a79
--- /dev/null
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/InfrastructureManager.kt
@@ -0,0 +1,115 @@
+package com.smoa.modules.communications.domain
+
+import com.smoa.core.common.CircuitBreaker
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Manages media infrastructure endpoints (STUN, TURN, signaling) with health and failover.
+ * Uses [CircuitBreaker] for stability; selects best available endpoint for [WebRTCConfig].
+ */
+@Singleton
+class InfrastructureManager @Inject constructor(
+ private val circuitBreaker: CircuitBreaker
+) {
+ private val _stunEndpoints = MutableStateFlow>(emptyList())
+ val stunEndpoints: StateFlow> = _stunEndpoints.asStateFlow()
+
+ private val _turnEndpoints = MutableStateFlow>(emptyList())
+ val turnEndpoints: StateFlow> = _turnEndpoints.asStateFlow()
+
+ private val _signalingEndpoints = MutableStateFlow>(emptyList())
+ val signalingEndpoints: StateFlow> = _signalingEndpoints.asStateFlow()
+
+ private val failureThreshold = 3
+ private val resetTimeoutMs = 60_000L
+
+ /**
+ * Configure STUN servers. Order defines preference; first healthy is used.
+ */
+ fun setStunEndpoints(urls: List) {
+ _stunEndpoints.value = urls.map { StunEndpoint(it) }
+ }
+
+ /**
+ * Configure TURN servers with optional credentials.
+ */
+ fun setTurnEndpoints(servers: List) {
+ _turnEndpoints.value = servers.map { TurnEndpoint(it.url, it.username, it.credential) }
+ }
+
+ /**
+ * Configure signaling server URLs for failover.
+ */
+ fun setSignalingEndpoints(urls: List) {
+ _signalingEndpoints.value = urls.map { SignalingEndpoint(it) }
+ }
+
+ /**
+ * Report success for an endpoint (resets its circuit breaker).
+ */
+ suspend fun reportSuccess(endpointId: String) {
+ circuitBreaker.reset(endpointId)
+ }
+
+ /**
+ * Report failure for an endpoint (increments circuit breaker).
+ */
+ suspend fun reportFailure(endpointId: String) {
+ circuitBreaker.recordFailure(endpointId)
+ }
+
+ /**
+ * Get best available STUN URLs (skipping open circuits).
+ */
+ fun getHealthyStunUrls(): List {
+ return _stunEndpoints.value
+ .filter { !circuitBreaker.isOpen(it.url, failureThreshold, resetTimeoutMs) }
+ .map { it.url }
+ }
+
+ /**
+ * Get best available TURN servers (skipping open circuits).
+ */
+ fun getHealthyTurnServers(): List {
+ return _turnEndpoints.value
+ .filter { !circuitBreaker.isOpen(it.url, failureThreshold, resetTimeoutMs) }
+ .map { TurnServer(it.url, it.username, it.credential) }
+ }
+
+ /**
+ * Get best available signaling URL (first healthy).
+ */
+ fun getHealthySignalingUrl(): String? {
+ return _signalingEndpoints.value
+ .firstOrNull { !circuitBreaker.isOpen(it.url, failureThreshold, resetTimeoutMs) }
+ ?.url
+ }
+
+ /**
+ * Build WebRTC config using current healthy endpoints.
+ */
+ fun buildWebRTCConfig(
+ defaultStun: List,
+ defaultTurn: List,
+ defaultSignalingUrl: String
+ ): WebRTCConfig {
+ val stunUrls = getHealthyStunUrls()
+ val turnServers = getHealthyTurnServers()
+ val signalingUrl = getHealthySignalingUrl()
+ return WebRTCConfig(
+ stunServers = if (stunUrls.isEmpty()) defaultStun else stunUrls.map { StunServer(it) },
+ turnServers = if (turnServers.isEmpty()) defaultTurn else turnServers.map { TurnServer(it.url, it.username, it.credential) },
+ signalingServerUrl = signalingUrl ?: defaultSignalingUrl,
+ iceCandidatePoolSize = 10,
+ mediaCodecPolicy = MediaCodecPolicy.default()
+ )
+ }
+}
+
+data class StunEndpoint(val url: String)
+data class TurnEndpoint(val url: String, val username: String? = null, val credential: String? = null)
+data class SignalingEndpoint(val url: String)
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/MediaRoutingPolicy.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/MediaRoutingPolicy.kt
new file mode 100644
index 0000000..7d3abb2
--- /dev/null
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/MediaRoutingPolicy.kt
@@ -0,0 +1,46 @@
+package com.smoa.modules.communications.domain
+
+import com.smoa.core.common.CellularGeneration
+import com.smoa.core.common.NetworkTransportType
+
+/**
+ * Policy for smart media routing: path preference and lag reduction.
+ * Used by [NetworkPathSelector] to choose the best network for voice/video.
+ * Supports 4G LTE, 5G, and 5G MW (millimeter wave) when on cellular.
+ */
+data class MediaRoutingPolicy(
+ /** Prefer low-latency transports (e.g. Wi-Fi, Ethernet over cellular). */
+ val preferLowLatency: Boolean = true,
+ /** When policy requires VPN, prefer VPN transport for media. */
+ val preferVpnWhenRequired: Boolean = true,
+ /** Transport preference order (first = highest). Default: WIFI, VPN, ETHERNET, CELLULAR. */
+ val transportPreferenceOrder: List = listOf(
+ NetworkTransportType.WIFI,
+ NetworkTransportType.VPN,
+ NetworkTransportType.ETHERNET,
+ NetworkTransportType.CELLULAR,
+ NetworkTransportType.UNKNOWN
+ ),
+ /** Within cellular: prefer 5G MW > 5G > 4G LTE for lower latency and higher capacity. */
+ val cellularGenerationPreferenceOrder: List = listOf(
+ CellularGeneration.NR_5G_MW,
+ CellularGeneration.NR_5G,
+ CellularGeneration.LTE_4G,
+ CellularGeneration.UNKNOWN
+ ),
+ /** Fall back to next-best path when current path quality degrades. */
+ val allowPathFailover: Boolean = true,
+ /** Minimum estimated bandwidth (kbps) to attempt video; below this use audio-only. */
+ val minBandwidthKbpsForVideo: Int = 128
+) {
+ fun rank(transport: NetworkTransportType): Int {
+ val index = transportPreferenceOrder.indexOf(transport)
+ return if (index < 0) Int.MAX_VALUE else index
+ }
+
+ fun rankCellularGeneration(generation: CellularGeneration?): Int {
+ if (generation == null) return Int.MAX_VALUE
+ val index = cellularGenerationPreferenceOrder.indexOf(generation)
+ return if (index < 0) Int.MAX_VALUE else index
+ }
+}
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/NetworkPathSelector.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/NetworkPathSelector.kt
new file mode 100644
index 0000000..3f81a5d
--- /dev/null
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/NetworkPathSelector.kt
@@ -0,0 +1,85 @@
+package com.smoa.modules.communications.domain
+
+import com.smoa.core.common.ConnectivityManager
+import com.smoa.core.common.NetworkTransportType
+import com.smoa.core.security.VPNManager
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Selects the best network path for media to reduce lag and improve QoS.
+ * Uses [ConnectivityManager] and [VPNManager] with [MediaRoutingPolicy].
+ */
+@Singleton
+class NetworkPathSelector @Inject constructor(
+ private val connectivityManager: ConnectivityManager,
+ private val vpnManager: VPNManager,
+ private val policy: MediaRoutingPolicy
+) {
+ private val _selectedPath = MutableStateFlow(selectedPathSync())
+ val selectedPath: StateFlow = _selectedPath.asStateFlow()
+
+ init {
+ // When connectivity or VPN changes, recompute path (caller can observe connectivityState/vpnState and call refresh())
+ }
+
+ /** Current best path for media. */
+ fun getSelectedPath(): SelectedPath = selectedPathSync()
+
+ /** Recompute and emit best path. Call when connectivity or VPN state changes. */
+ fun refresh() {
+ _selectedPath.value = selectedPathSync()
+ }
+
+ private fun selectedPathSync(): SelectedPath {
+ if (connectivityManager.isOffline() || connectivityManager.isRestricted()) {
+ return SelectedPath(
+ transport = NetworkTransportType.UNKNOWN,
+ cellularGeneration = null,
+ recommendedForVideo = false,
+ reason = "Offline or restricted"
+ )
+ }
+ val transport = connectivityManager.getActiveTransportType()
+ val vpnRequired = vpnManager.isVPNRequired()
+ val vpnConnected = vpnManager.isVPNConnected()
+ val effectiveTransport = if (vpnRequired && !vpnConnected) {
+ NetworkTransportType.UNKNOWN
+ } else {
+ transport
+ }
+ val cellularGeneration = if (effectiveTransport == NetworkTransportType.CELLULAR) {
+ connectivityManager.getCellularGeneration()
+ } else null
+ val transportRank = policy.rank(effectiveTransport)
+ val cellularRank = policy.rankCellularGeneration(cellularGeneration)
+ val rank = if (effectiveTransport == NetworkTransportType.CELLULAR && cellularGeneration != null) {
+ transportRank * 10 + cellularRank
+ } else transportRank
+ val recommendedForVideo = connectivityManager.isOnline() &&
+ effectiveTransport != NetworkTransportType.UNKNOWN &&
+ policy.minBandwidthKbpsForVideo > 0
+ return SelectedPath(
+ transport = effectiveTransport,
+ cellularGeneration = cellularGeneration,
+ rank = rank,
+ recommendedForVideo = recommendedForVideo,
+ reason = if (vpnRequired && !vpnConnected) "VPN required" else null
+ )
+ }
+}
+
+/**
+ * Result of path selection for media.
+ * When [transport] is CELLULAR, [cellularGeneration] is 4G LTE, 5G, or 5G MW.
+ */
+data class SelectedPath(
+ val transport: NetworkTransportType,
+ val cellularGeneration: com.smoa.core.common.CellularGeneration? = null,
+ val rank: Int = Int.MAX_VALUE,
+ val recommendedForVideo: Boolean = false,
+ val reason: String? = null
+)
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/SmartRoutingService.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/SmartRoutingService.kt
new file mode 100644
index 0000000..d81830b
--- /dev/null
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/SmartRoutingService.kt
@@ -0,0 +1,163 @@
+package com.smoa.modules.communications.domain
+
+import com.smoa.core.common.QoSPolicy
+import com.smoa.core.common.TrafficClass
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Orchestrates smart routing for better QoS, lag reduction, infra management, and system stability.
+ * Combines [NetworkPathSelector], [InfrastructureManager], [ConnectionStabilityController],
+ * and [AdaptiveCodecSelector] into a single service for the communications/meetings stack.
+ */
+@Singleton
+class SmartRoutingService @Inject constructor(
+ private val networkPathSelector: NetworkPathSelector,
+ private val infrastructureManager: InfrastructureManager,
+ private val stabilityController: ConnectionStabilityController,
+ private val adaptiveCodecSelector: AdaptiveCodecSelector,
+ private val connectionQualityMonitor: ConnectionQualityMonitor
+) {
+ private val _routingState = MutableStateFlow(RoutingState())
+ val routingState: StateFlow = _routingState.asStateFlow()
+
+ init {
+ // Expose combined state for UI or WebRTC layer
+ // _routingState can be updated from path + stability + quality
+ refreshState()
+ }
+
+ /**
+ * Current best path, degradation, and infra summary.
+ */
+ fun getRoutingState(): RoutingState = _routingState.value
+
+ /**
+ * Recompute routing state (path, degradation, backoff). Call when connectivity or quality changes.
+ */
+ fun refreshState() {
+ val path = networkPathSelector.getSelectedPath()
+ val degradation = stabilityController.degradationMode.value
+ val backoffMs = stabilityController.reconnectBackoffMs.value
+ val sessionCount = stabilityController.activeSessionCount.value
+ val quality = connectionQualityMonitor.currentQuality.value
+ _routingState.value = RoutingState(
+ selectedPath = path,
+ degradationMode = degradation,
+ reconnectBackoffMs = backoffMs,
+ activeSessionCount = sessionCount,
+ connectionTier = quality.tier,
+ recommendedForVideo = path.recommendedForVideo && !stabilityController.shouldDisableVideo()
+ )
+ }
+
+ /**
+ * Apply connection tier from quality monitor to codec selector and optionally trigger degradation.
+ */
+ fun updateFromConnectionQuality() {
+ val quality = connectionQualityMonitor.currentQuality.value
+ adaptiveCodecSelector.selectForTier(quality.tier)
+ if (quality.tier == ConnectionTier.VERY_LOW) {
+ stabilityController.setDegradationMode(DegradationMode.AUDIO_ONLY)
+ } else if (quality.tier == ConnectionTier.LOW && stabilityController.degradationMode.value == DegradationMode.AUDIO_ONLY) {
+ stabilityController.setDegradationMode(DegradationMode.NONE)
+ }
+ refreshState()
+ }
+
+ /**
+ * Notify path/connectivity changed (e.g. from ConnectivityManager callback).
+ */
+ fun onConnectivityChanged() {
+ networkPathSelector.refresh()
+ refreshState()
+ }
+
+ /**
+ * Get WebRTC config with healthy infra endpoints.
+ */
+ fun getWebRTCConfig(): WebRTCConfig {
+ return infrastructureManager.buildWebRTCConfig(
+ defaultStun = WebRTCConfig.default().stunServers,
+ defaultTurn = WebRTCConfig.default().turnServers,
+ defaultSignalingUrl = WebRTCConfig.default().signalingServerUrl
+ )
+ }
+
+ /**
+ * Set QoS policy for stability (session cap, bitrate cap).
+ */
+ fun setQoSPolicy(policy: QoSPolicy) {
+ stabilityController.setQoSPolicy(policy)
+ refreshState()
+ }
+
+ /**
+ * Record connection failure and return backoff before retry.
+ */
+ fun recordConnectionFailure(): Long {
+ val backoff = stabilityController.recordConnectionFailure()
+ refreshState()
+ return backoff
+ }
+
+ /**
+ * Record connection success (resets backoff).
+ */
+ fun recordConnectionSuccess() {
+ stabilityController.recordConnectionSuccess()
+ refreshState()
+ }
+
+ /**
+ * Report endpoint failure for infra failover.
+ */
+ suspend fun reportEndpointFailure(endpointId: String) {
+ infrastructureManager.reportFailure(endpointId)
+ refreshState()
+ }
+
+ /**
+ * Report endpoint success (resets circuit for that endpoint).
+ */
+ suspend fun reportEndpointSuccess(endpointId: String) {
+ infrastructureManager.reportSuccess(endpointId)
+ }
+
+ /**
+ * Priority for traffic class (QoS scheduling hint).
+ */
+ fun priorityForTrafficClass(trafficClass: TrafficClass): Int = trafficClass.priority
+
+ /**
+ * Try to start a media session (respects QoS session cap). Returns true if started.
+ */
+ fun tryStartSession(): Boolean {
+ val ok = stabilityController.notifySessionStarted()
+ if (ok) refreshState()
+ return ok
+ }
+
+ /**
+ * Notify that a media session ended (for session cap and stability).
+ */
+ fun notifySessionEnded() {
+ stabilityController.notifySessionEnded()
+ refreshState()
+ }
+}
+
+/**
+ * Combined smart routing state for UI or media layer.
+ */
+data class RoutingState(
+ val selectedPath: SelectedPath? = null,
+ val degradationMode: DegradationMode = DegradationMode.NONE,
+ val reconnectBackoffMs: Long = 0L,
+ val activeSessionCount: Int = 0,
+ val connectionTier: ConnectionTier = ConnectionTier.MEDIUM,
+ val recommendedForVideo: Boolean = true
+)
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/StubConnectionQualityMonitor.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/StubConnectionQualityMonitor.kt
new file mode 100644
index 0000000..6af3c7a
--- /dev/null
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/StubConnectionQualityMonitor.kt
@@ -0,0 +1,40 @@
+package com.smoa.modules.communications.domain
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Stub implementation of [ConnectionQualityMonitor].
+ * Reports a fixed MEDIUM tier until WebRTC stats are integrated; then replace
+ * with an implementation that parses RTCStatsReport (e.g. outbound-rtp,
+ * remote-inbound-rtp, candidate-pair) to compute estimated bandwidth, RTT, and loss.
+ */
+@Singleton
+class StubConnectionQualityMonitor @Inject constructor() : ConnectionQualityMonitor {
+ private val _currentQuality = MutableStateFlow(
+ ConnectionQuality(
+ estimatedBandwidthKbps = 384,
+ rttMs = 80,
+ packetLossFraction = 0f,
+ tier = ConnectionTier.MEDIUM
+ )
+ )
+ override val currentQuality: StateFlow = _currentQuality.asStateFlow()
+
+ override fun qualityUpdates(): Flow = currentQuality
+
+ /** Update quality (e.g. from WebRTC getStats callback). */
+ fun update(estimatedBandwidthKbps: Int, rttMs: Int = -1, packetLoss: Float = -1f) {
+ val tier = connectionTierFromBandwidth(estimatedBandwidthKbps, rttMs, packetLoss)
+ _currentQuality.value = ConnectionQuality(
+ estimatedBandwidthKbps = estimatedBandwidthKbps,
+ rttMs = rttMs,
+ packetLossFraction = packetLoss,
+ tier = tier
+ )
+ }
+}
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/VoiceTransport.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/VoiceTransport.kt
index 6e2d98a..4aa30fa 100644
--- a/modules/communications/src/main/java/com/smoa/modules/communications/domain/VoiceTransport.kt
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/VoiceTransport.kt
@@ -13,7 +13,8 @@ import javax.inject.Singleton
*/
@Singleton
class VoiceTransport @Inject constructor(
- private val webRTCManager: WebRTCManager
+ private val webRTCManager: WebRTCManager,
+ private val smartRoutingService: SmartRoutingService
) {
private val _connectionState = MutableStateFlow(ConnectionState.Disconnected)
val connectionState: StateFlow = _connectionState.asStateFlow()
@@ -27,19 +28,21 @@ class VoiceTransport @Inject constructor(
*/
suspend fun joinChannel(channelId: String): Result {
return try {
+ if (!smartRoutingService.tryStartSession()) {
+ return Result.Error(IllegalStateException("Session cap reached"))
+ }
_connectionState.value = ConnectionState.Connecting(channelId)
-
- // Initialize WebRTC peer connection (audio only for voice)
val connectionResult = webRTCManager.initializePeerConnection(channelId, isAudioOnly = true)
-
when (connectionResult) {
is Result.Success -> {
peerConnection = connectionResult.data
currentChannelId = channelId
+ smartRoutingService.recordConnectionSuccess()
_connectionState.value = ConnectionState.Connected(channelId)
Result.Success(Unit)
}
is Result.Error -> {
+ smartRoutingService.recordConnectionFailure()
_connectionState.value = ConnectionState.Error(connectionResult.exception.message ?: "Failed to connect")
Result.Error(connectionResult.exception)
}
@@ -49,6 +52,7 @@ class VoiceTransport @Inject constructor(
}
}
} catch (e: Exception) {
+ smartRoutingService.recordConnectionFailure()
_connectionState.value = ConnectionState.Error(e.message ?: "Unknown error")
Result.Error(e)
}
@@ -70,6 +74,7 @@ class VoiceTransport @Inject constructor(
peerConnection = null
currentChannelId = null
+ smartRoutingService.notifySessionEnded()
_connectionState.value = ConnectionState.Disconnected
Result.Success(Unit)
} catch (e: Exception) {
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/WebRTCConfig.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/WebRTCConfig.kt
index 702efea..246e852 100644
--- a/modules/communications/src/main/java/com/smoa/modules/communications/domain/WebRTCConfig.kt
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/WebRTCConfig.kt
@@ -1,13 +1,16 @@
package com.smoa.modules.communications.domain
/**
- * WebRTC configuration for STUN/TURN servers and signaling.
+ * WebRTC configuration for STUN/TURN servers, signaling, and optional
+ * connection-speed-aware media (audio/video codec) policy.
*/
data class WebRTCConfig(
val stunServers: List,
val turnServers: List,
val signalingServerUrl: String,
- val iceCandidatePoolSize: Int = 10
+ val iceCandidatePoolSize: Int = 10,
+ /** When set, codec and bitrate are chosen from this policy based on connection speed. */
+ val mediaCodecPolicy: MediaCodecPolicy? = null
) {
companion object {
/**
@@ -20,9 +23,10 @@ data class WebRTCConfig(
StunServer("stun:stun.l.google.com:19302"),
StunServer("stun:stun1.l.google.com:19302")
),
- turnServers = emptyList(), // TURN servers should be configured per deployment
- signalingServerUrl = "", // Should be configured per deployment
- iceCandidatePoolSize = 10
+ turnServers = emptyList(),
+ signalingServerUrl = "",
+ iceCandidatePoolSize = 10,
+ mediaCodecPolicy = MediaCodecPolicy.default()
)
}
}
diff --git a/modules/communications/src/main/java/com/smoa/modules/communications/domain/WebRTCManager.kt b/modules/communications/src/main/java/com/smoa/modules/communications/domain/WebRTCManager.kt
index 8ecb794..c27148a 100644
--- a/modules/communications/src/main/java/com/smoa/modules/communications/domain/WebRTCManager.kt
+++ b/modules/communications/src/main/java/com/smoa/modules/communications/domain/WebRTCManager.kt
@@ -14,9 +14,11 @@ import javax.inject.Singleton
*/
@Singleton
class WebRTCManager @Inject constructor(
- private val context: Context
+ private val context: Context,
+ private val adaptiveCodecSelector: AdaptiveCodecSelector,
+ private val smartRoutingService: SmartRoutingService
) {
- private val config = WebRTCConfig.default()
+ private fun getConfig(): WebRTCConfig = smartRoutingService.getWebRTCConfig()
private val peerConnections = mutableMapOf()
private val _connectionState = MutableStateFlow(WebRTCConnectionState.Disconnected)
val connectionState: StateFlow = _connectionState.asStateFlow()
@@ -62,9 +64,9 @@ class WebRTCManager @Inject constructor(
* Create RTC configuration with STUN/TURN servers.
*/
private fun createRTCConfiguration(): RTCConfiguration {
+ val config = getConfig()
val iceServers = mutableListOf()
- // Add STUN servers
config.stunServers.forEach { stunServer ->
iceServers.add(IceServer(stunServer.url))
}
@@ -80,9 +82,14 @@ class WebRTCManager @Inject constructor(
)
}
+ val policy = config.mediaCodecPolicy
+ val audioConstraints = if (policy != null) adaptiveCodecSelector.getAudioConstraints() else null
+ val videoConstraints = if (policy != null) adaptiveCodecSelector.getVideoConstraints() else null
return RTCConfiguration(
iceServers = iceServers,
- iceCandidatePoolSize = config.iceCandidatePoolSize
+ iceCandidatePoolSize = config.iceCandidatePoolSize,
+ audioConstraints = audioConstraints,
+ videoConstraints = videoConstraints
)
}
@@ -211,10 +218,14 @@ data class WebRTCPeerConnection(
/**
* RTC configuration for peer connections.
+ * When connection-speed-aware codecs are enabled, audioConstraints and videoConstraints
+ * are set from [AdaptiveCodecSelector] so encoding uses the appropriate codec and bitrate.
*/
data class RTCConfiguration(
val iceServers: List,
- val iceCandidatePoolSize: Int = 10
+ val iceCandidatePoolSize: Int = 10,
+ val audioConstraints: AudioCodecConstraints? = null,
+ val videoConstraints: VideoCodecConstraints? = null
)
/**
diff --git a/modules/meetings/src/main/java/com/smoa/modules/meetings/domain/VideoTransport.kt b/modules/meetings/src/main/java/com/smoa/modules/meetings/domain/VideoTransport.kt
index 1a85930..a820722 100644
--- a/modules/meetings/src/main/java/com/smoa/modules/meetings/domain/VideoTransport.kt
+++ b/modules/meetings/src/main/java/com/smoa/modules/meetings/domain/VideoTransport.kt
@@ -14,7 +14,8 @@ import javax.inject.Singleton
*/
@Singleton
class VideoTransport @Inject constructor(
- private val webRTCManager: com.smoa.modules.communications.domain.WebRTCManager
+ private val webRTCManager: com.smoa.modules.communications.domain.WebRTCManager,
+ private val smartRoutingService: com.smoa.modules.communications.domain.SmartRoutingService
) {
private val _connectionState = MutableStateFlow(MeetingConnectionState.Disconnected)
val connectionState: StateFlow = _connectionState.asStateFlow()
@@ -29,26 +30,30 @@ class VideoTransport @Inject constructor(
*/
suspend fun joinMeeting(meetingId: String, userId: String): Result {
return try {
+ if (!smartRoutingService.tryStartSession()) {
+ return Result.Error(IllegalStateException("Session cap reached"))
+ }
_connectionState.value = MeetingConnectionState.Connecting(meetingId)
-
- // Initialize WebRTC peer connection (audio + video)
- val connectionResult = webRTCManager.initializePeerConnection(meetingId, isAudioOnly = false)
-
+ val routingState = smartRoutingService.getRoutingState()
+ val recommendedForVideo = routingState.recommendedForVideo
+ val isAudioOnly = !recommendedForVideo
+
+ val connectionResult = webRTCManager.initializePeerConnection(meetingId, isAudioOnly = isAudioOnly)
+
when (connectionResult) {
is Result.Success -> {
peerConnection = connectionResult.data
currentMeetingId = meetingId
-
- // Start audio and video transmission
+ smartRoutingService.recordConnectionSuccess()
peerConnection?.let { connection ->
webRTCManager.startAudioTransmission(connection)
- webRTCManager.startVideoTransmission(connection)
+ if (!isAudioOnly) webRTCManager.startVideoTransmission(connection)
}
-
_connectionState.value = MeetingConnectionState.Connected(meetingId)
Result.Success(Unit)
}
is Result.Error -> {
+ smartRoutingService.recordConnectionFailure()
_connectionState.value = MeetingConnectionState.Error(
connectionResult.exception.message ?: "Failed to connect"
)
@@ -60,6 +65,7 @@ class VideoTransport @Inject constructor(
}
}
} catch (e: Exception) {
+ smartRoutingService.recordConnectionFailure()
_connectionState.value = MeetingConnectionState.Error(e.message ?: "Unknown error")
Result.Error(e)
}
@@ -83,6 +89,7 @@ class VideoTransport @Inject constructor(
peerConnection = null
currentMeetingId = null
+ smartRoutingService.notifySessionEnded()
_connectionState.value = MeetingConnectionState.Disconnected
Result.Success(Unit)
} catch (e: Exception) {
diff --git a/modules/reports/src/main/java/com/smoa/modules/reports/domain/ReportService.kt b/modules/reports/src/main/java/com/smoa/modules/reports/domain/ReportService.kt
index 8be5bae..8d1708e 100644
--- a/modules/reports/src/main/java/com/smoa/modules/reports/domain/ReportService.kt
+++ b/modules/reports/src/main/java/com/smoa/modules/reports/domain/ReportService.kt
@@ -2,6 +2,7 @@ package com.smoa.modules.reports.domain
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
+import java.security.MessageDigest
import java.util.Date
import java.util.UUID
import javax.inject.Inject
@@ -15,7 +16,10 @@ class ReportService @Inject constructor(
private val reportGenerator: ReportGenerator,
private val auditLogger: AuditLogger
) {
-
+
+ /** When true, reports get a minimal content-hash signature; for full signing use a dedicated signing service. */
+ var signReports: Boolean = false
+
/**
* Generate report.
*/
@@ -28,6 +32,14 @@ class ReportService @Inject constructor(
template: ReportTemplate?
): Result {
return try {
+ val signature = if (signReports) {
+ DigitalSignature(
+ signatureId = UUID.randomUUID().toString(),
+ signerId = generatedBy,
+ signatureDate = Date(),
+ signatureData = MessageDigest.getInstance("SHA-256").digest(content)
+ )
+ } else null
val report = Report(
reportId = UUID.randomUUID().toString(),
reportType = reportType,
@@ -37,7 +49,7 @@ class ReportService @Inject constructor(
content = content,
generatedDate = Date(),
generatedBy = generatedBy,
- signature = null, // TODO: Add digital signature
+ signature = signature,
metadata = ReportMetadata()
)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a9e679e..170d369 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -39,4 +39,5 @@ include(":modules:ncic")
include(":modules:military")
include(":modules:judicial")
include(":modules:intelligence")
+include(":backend")