Skip to content

Commit e951602

Browse files
committed
Support registering custom HTTP endpoints
1 parent 385f7d2 commit e951602

File tree

9 files changed

+480
-21
lines changed

9 files changed

+480
-21
lines changed

bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsProvider.kt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package com.penumbraos.bridge_settings
22

33
import android.util.Log
44
import com.penumbraos.bridge.ISettingsProvider
5+
import com.penumbraos.bridge.callback.IHttpEndpointCallback
56
import com.penumbraos.bridge.callback.ISettingsCallback
67
import com.penumbraos.bridge_settings.providers.safeCallback
8+
import com.penumbraos.bridge_settings.server.AidlEndpointCallback
79
import kotlinx.coroutines.CoroutineScope
810
import kotlinx.coroutines.Dispatchers
911
import kotlinx.coroutines.SupervisorJob
@@ -22,6 +24,8 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
2224
private val settingListeners = ConcurrentHashMap<String, MutableSet<SettingListener>>()
2325
private val providerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
2426

27+
private var webServer: SettingsWebServer? = null
28+
2529
private data class SettingListener(
2630
val callback: ISettingsCallback,
2731
val type: String
@@ -323,6 +327,78 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
323327
settingListeners.clear()
324328
}
325329

330+
fun setWebServer(webServer: SettingsWebServer) {
331+
this.webServer = webServer
332+
}
333+
334+
override fun registerHttpEndpoint(
335+
providerId: String,
336+
path: String,
337+
method: String,
338+
callback: IHttpEndpointCallback
339+
): Boolean {
340+
return try {
341+
val server = webServer
342+
if (server == null) {
343+
Log.e(TAG, "Cannot register HTTP endpoint - web server not initialized")
344+
false
345+
} else {
346+
val success = server.registerEndpoint(
347+
providerId,
348+
path,
349+
method,
350+
AidlEndpointCallback(callback)
351+
)
352+
Log.i(
353+
TAG,
354+
"Registered HTTP endpoint: $method $path for provider $providerId - success: $success"
355+
)
356+
success
357+
}
358+
} catch (e: Exception) {
359+
Log.e(TAG, "Error registering HTTP endpoint: $providerId $method $path", e)
360+
false
361+
}
362+
}
363+
364+
override fun unregisterHttpEndpoint(
365+
providerId: String,
366+
path: String,
367+
method: String
368+
): Boolean {
369+
return try {
370+
val server = webServer
371+
if (server == null) {
372+
Log.e(TAG, "Cannot unregister HTTP endpoint - web server not initialized")
373+
false
374+
} else {
375+
val success = server.unregisterEndpoint(providerId, path, method)
376+
Log.i(
377+
TAG,
378+
"Unregistered HTTP endpoint: $method $path for provider $providerId - success: $success"
379+
)
380+
success
381+
}
382+
} catch (e: Exception) {
383+
Log.e(TAG, "Error unregistering HTTP endpoint: $providerId $method $path", e)
384+
false
385+
}
386+
}
387+
388+
override fun unregisterAllHttpEndpoints(providerId: String) {
389+
try {
390+
val server = webServer
391+
if (server == null) {
392+
Log.e(TAG, "Cannot unregister HTTP endpoints - web server not initialized")
393+
} else {
394+
server.unregisterAllEndpointsForProvider(providerId)
395+
Log.i(TAG, "Unregistered all HTTP endpoints for provider: $providerId")
396+
}
397+
} catch (e: Exception) {
398+
Log.e(TAG, "Error unregistering all HTTP endpoints for provider: $providerId", e)
399+
}
400+
}
401+
326402
// Discovery methods for dynamic registration
327403
override fun getAvailableSystemSettings(): List<com.penumbraos.bridge.types.SystemSettingInfo> {
328404
return try {

bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class SettingsService {
7575

7676
// Connect registry to web server for broadcasting
7777
settingsRegistry.setWebServer(webServer)
78+
settingsProvider.setWebServer(webServer)
7879

7980
webServer.start()
8081

bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsWebServer.kt

Lines changed: 128 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@ package com.penumbraos.bridge_settings
44

55
import android.util.Log
66
import com.penumbraos.bridge_settings.json.toJsonElement
7+
import com.penumbraos.bridge_settings.server.EndpointCallback
8+
import com.penumbraos.bridge_settings.server.EndpointRequest
9+
import com.penumbraos.bridge_settings.server.RegisteredEndpoint
710
import io.ktor.http.ContentType
811
import io.ktor.http.HttpStatusCode
912
import io.ktor.serialization.kotlinx.json.json
1013
import io.ktor.server.application.Application
14+
import io.ktor.server.application.ApplicationCallPipeline
15+
import io.ktor.server.application.call
1116
import io.ktor.server.application.install
1217
import io.ktor.server.engine.EmbeddedServer
1318
import io.ktor.server.engine.embeddedServer
1419
import io.ktor.server.netty.Netty
1520
import io.ktor.server.netty.NettyApplicationEngine
1621
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
22+
import io.ktor.server.request.httpMethod
1723
import io.ktor.server.request.receive
24+
import io.ktor.server.request.receiveText
25+
import io.ktor.server.request.uri
1826
import io.ktor.server.response.respond
1927
import io.ktor.server.response.respondBytes
2028
import io.ktor.server.response.respondText
@@ -25,6 +33,7 @@ import io.ktor.server.websocket.WebSockets
2533
import io.ktor.server.websocket.pingPeriod
2634
import io.ktor.server.websocket.timeout
2735
import io.ktor.server.websocket.webSocket
36+
import io.ktor.util.toMap
2837
import io.ktor.websocket.DefaultWebSocketSession
2938
import io.ktor.websocket.Frame
3039
import io.ktor.websocket.readText
@@ -139,9 +148,55 @@ class SettingsWebServer(
139148
private val webSocketSessions = ConcurrentHashMap<String, DefaultWebSocketSession>()
140149
private val serverScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
141150
private val logStreamProvider = LogStreamProvider()
151+
private val registeredEndpoints = ConcurrentHashMap<String, RegisteredEndpoint>()
142152

143153
fun getLogStreamProvider(): LogStreamProvider = logStreamProvider
144154

155+
fun registerEndpoint(
156+
providerId: String,
157+
path: String,
158+
method: String,
159+
callback: EndpointCallback
160+
): Boolean {
161+
val normalizedPath = if (path.startsWith("/")) path else "/$path"
162+
val endpointKey = "${method.uppercase()}:$normalizedPath"
163+
164+
if (registeredEndpoints.containsKey(endpointKey)) {
165+
Log.w(TAG, "Endpoint already registered: $endpointKey")
166+
return false
167+
}
168+
169+
val endpoint = RegisteredEndpoint(normalizedPath, method.uppercase(), callback, providerId)
170+
registeredEndpoints[endpointKey] = endpoint
171+
Log.i(TAG, "Registered endpoint: $endpointKey for provider: $providerId")
172+
return true
173+
}
174+
175+
fun unregisterEndpoint(providerId: String, path: String, method: String): Boolean {
176+
val normalizedPath = if (path.startsWith("/")) path else "/$path"
177+
val endpointKey = "${method.uppercase()}:$normalizedPath"
178+
179+
val endpoint = registeredEndpoints[endpointKey]
180+
if (endpoint?.providerId == providerId) {
181+
registeredEndpoints.remove(endpointKey)
182+
Log.i(TAG, "Unregistered endpoint: $endpointKey for provider: $providerId")
183+
return true
184+
}
185+
186+
Log.w(TAG, "Cannot unregister endpoint $endpointKey - not found or wrong provider")
187+
return false
188+
}
189+
190+
fun unregisterAllEndpointsForProvider(providerId: String) {
191+
val toRemove = registeredEndpoints.entries.filter { it.value.providerId == providerId }
192+
toRemove.forEach { registeredEndpoints.remove(it.key) }
193+
Log.i(TAG, "Unregistered ${toRemove.size} endpoints for provider: $providerId")
194+
}
195+
196+
fun getRegisteredEndpoints(): List<RegisteredEndpoint> {
197+
return registeredEndpoints.values.toList()
198+
}
199+
145200
suspend fun start() {
146201
Log.i(TAG, "Starting settings web server on port $port")
147202

@@ -224,12 +279,63 @@ class SettingsWebServer(
224279
masking = false
225280
}
226281

282+
intercept(ApplicationCallPipeline.Call) {
283+
val fullPath =
284+
call.request.uri.substringBefore('?') // Remove query params from path
285+
val method = call.request.httpMethod.value
286+
val endpointKey = "${method}:$fullPath"
287+
288+
val endpoint = registeredEndpoints[endpointKey]
289+
if (endpoint != null) {
290+
try {
291+
val headers = call.request.headers.toMap()
292+
.mapValues { it.value.firstOrNull() ?: "" }
293+
val queryParams = call.request.queryParameters.toMap()
294+
.mapValues { it.value.firstOrNull() ?: "" }
295+
val body = try {
296+
call.receiveText()
297+
} catch (e: Exception) {
298+
null
299+
}
300+
301+
val request = EndpointRequest(
302+
path = fullPath,
303+
method = method,
304+
headers = headers,
305+
queryParams = queryParams,
306+
body = body
307+
)
308+
309+
val response = endpoint.callback.handle(request)
310+
311+
response.headers.forEach { (key, value) ->
312+
call.response.headers.append(key, value)
313+
}
314+
315+
val contentType = ContentType.parse(response.contentType)
316+
call.respondText(
317+
response.body,
318+
contentType,
319+
HttpStatusCode.fromValue(response.statusCode)
320+
)
321+
return@intercept finish()
322+
} catch (e: Exception) {
323+
Log.e(TAG, "Error handling dynamic endpoint $endpointKey", e)
324+
call.respond(
325+
HttpStatusCode.InternalServerError,
326+
mapOf("error" to "Internal server error")
327+
)
328+
return@intercept finish()
329+
}
330+
}
331+
}
332+
227333
routing {
228334
// WebSocket endpoint for real-time communication
229335
webSocket("/ws/settings") {
230336
handleWebSocketConnection(this)
231337
}
232-
338+
233339
// REST API endpoints
234340
get("/api/settings") {
235341
try {
@@ -273,7 +379,8 @@ class SettingsWebServer(
273379
get("/api/settings/app/{appId}") {
274380
try {
275381
val appId =
276-
call.parameters["appId"] ?: throw IllegalArgumentException("Missing appId")
382+
call.parameters["appId"]
383+
?: throw IllegalArgumentException("Missing appId")
277384
val appSettings = settingsRegistry.getAllAppSettings(appId)
278385
call.respond(appSettings)
279386
} catch (e: Exception) {
@@ -285,7 +392,8 @@ class SettingsWebServer(
285392
post("/api/settings/app/{appId}/{category}/{key}") {
286393
try {
287394
val appId =
288-
call.parameters["appId"] ?: throw IllegalArgumentException("Missing appId")
395+
call.parameters["appId"]
396+
?: throw IllegalArgumentException("Missing appId")
289397
val category = call.parameters["category"]
290398
?: throw IllegalArgumentException("Missing category")
291399
val key =
@@ -334,7 +442,10 @@ class SettingsWebServer(
334442
)
335443
call.respondBytes(zipBytes)
336444

337-
Log.i(TAG, "Logs zip generated and sent: $filename (${zipBytes.size} bytes)")
445+
Log.i(
446+
TAG,
447+
"Logs zip generated and sent: $filename (${zipBytes.size} bytes)"
448+
)
338449
} catch (e: Exception) {
339450
Log.e(TAG, "Error generating logs zip", e)
340451
call.respond(
@@ -353,8 +464,11 @@ class SettingsWebServer(
353464
val inputStream = getResourceFromApk(resourcePath)
354465
if (inputStream != null) {
355466
val contentType = getContentType(resourcePath)
356-
357-
call.response.headers.append("Cache-Control", "no-cache, no-store, must-revalidate")
467+
468+
call.response.headers.append(
469+
"Cache-Control",
470+
"no-cache, no-store, must-revalidate"
471+
)
358472
call.response.headers.append("Pragma", "no-cache")
359473
call.response.headers.append("Expires", "0")
360474

@@ -364,7 +478,10 @@ class SettingsWebServer(
364478
// File not found, try to serve index.html for SPA routing
365479
val indexStream = getResourceFromApk("react-build/index.html")
366480
if (indexStream != null) {
367-
call.response.headers.append("Cache-Control", "no-cache, no-store, must-revalidate")
481+
call.response.headers.append(
482+
"Cache-Control",
483+
"no-cache, no-store, must-revalidate"
484+
)
368485
call.response.headers.append("Pragma", "no-cache")
369486
call.response.headers.append("Expires", "0")
370487

@@ -383,7 +500,10 @@ class SettingsWebServer(
383500
get("/") {
384501
val indexStream = getResourceFromApk("react-build/index.html")
385502
if (indexStream != null) {
386-
call.response.headers.append("Cache-Control", "no-cache, no-store, must-revalidate")
503+
call.response.headers.append(
504+
"Cache-Control",
505+
"no-cache, no-store, must-revalidate"
506+
)
387507
call.response.headers.append("Pragma", "no-cache")
388508
call.response.headers.append("Expires", "0")
389509

0 commit comments

Comments
 (0)