App Settings
-
- {appSettingsEntries.map(([fullKey, settings]) => {
- const appName = fullKey.split('.')[0];
- const categoryName = fullKey.split('.').slice(1).join('.');
-
- return (
-
-
- {appName} - {categoryName}
-
-
- {Object.entries(settings).map(([settingKey, value]) => (
-
-
{settingKey}
+ {appSettingsEntries.map(([appId, settings]) => (
+
+
+ {appId}
+
+ {Object.entries(settings).map(([category, categoryContents]) => {
+ return Object.entries(
+ categoryContents as Record
+ ).map(([property, value]) => (
+
+
{property}
- {renderSettingControl(fullKey, settingKey, value)}
+
- ))}
-
- );
- })}
+ ));
+ })}
+
+ ))}
);
};
-export default AppSettings;
\ No newline at end of file
+const SettingControlView: React.FC<{
+ appId: string;
+ category: string;
+ property: string;
+ value: unknown;
+}> = ({ appId, category, property, value }) => {
+ const { updateAppSetting } = useSettings();
+
+ if (typeof value === "boolean") {
+ return (
+
+ updateAppSetting(appId, category, property, enabled)
+ }
+ />
+ );
+ } else if (typeof value === "number") {
+ return (
+
+
+ updateAppSetting(appId, category, property, newValue)
+ }
+ />
+ {value}
+
+ );
+ } else {
+ return (
+
+ updateAppSetting(appId, category, property, newValue)
+ }
+ />
+ );
+ }
+};
diff --git a/bridge-settings/react-app/src/components/Slider.tsx b/bridge-settings/react-app/src/components/Slider.tsx
index 176cf44..bd300ee 100644
--- a/bridge-settings/react-app/src/components/Slider.tsx
+++ b/bridge-settings/react-app/src/components/Slider.tsx
@@ -1,21 +1,21 @@
-import React from 'react';
+import React from "react";
interface SliderProps {
value: number;
- min: number;
- max: number;
+ min?: number;
+ max?: number;
step?: number;
onChange: (value: number) => void;
disabled?: boolean;
}
-const Slider: React.FC = ({
- value,
- min,
- max,
- step = 1,
- onChange,
- disabled = false
+const Slider: React.FC = ({
+ value,
+ min,
+ max,
+ step = 1,
+ onChange,
+ disabled = false,
}) => {
const handleChange = (event: React.ChangeEvent) => {
if (!disabled) {
@@ -38,4 +38,4 @@ const Slider: React.FC = ({
);
};
-export default Slider;
\ No newline at end of file
+export default Slider;
diff --git a/bridge-settings/react-app/src/components/TextInput.tsx b/bridge-settings/react-app/src/components/TextInput.tsx
new file mode 100644
index 0000000..f5ba606
--- /dev/null
+++ b/bridge-settings/react-app/src/components/TextInput.tsx
@@ -0,0 +1,47 @@
+import React from "react";
+
+interface TextInputProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+ disabled?: boolean;
+ style?: React.CSSProperties;
+}
+
+const TextInput: React.FC = ({
+ value,
+ onChange,
+ placeholder = "",
+ disabled = false,
+ style = {},
+}) => {
+ const handleChange = (e: React.ChangeEvent) => {
+ onChange(e.target.value);
+ };
+
+ const defaultStyle: React.CSSProperties = {
+ padding: "4px 8px",
+ border: "1px solid #bdc3c7",
+ borderRadius: "3px",
+ fontSize: "0.9rem",
+ minWidth: "120px",
+ maxWidth: "200px",
+ outline: "none",
+ ...style,
+ };
+
+ return (
+ (e.target.style.borderColor = "#3498db")}
+ onBlur={(e) => (e.target.style.borderColor = "#bdc3c7")}
+ />
+ );
+};
+
+export default TextInput;
diff --git a/bridge-settings/react-app/src/hooks/useSettings.ts b/bridge-settings/react-app/src/hooks/useSettings.ts
index 3bb7969..deb9e50 100644
--- a/bridge-settings/react-app/src/hooks/useSettings.ts
+++ b/bridge-settings/react-app/src/hooks/useSettings.ts
@@ -127,18 +127,15 @@ export function useSettings() {
}
}, [lastMessage]);
- const updateSystemSetting = (key: string, value: unknown) => {
- updateSetting("system", key, value);
- };
+ const updateSystemSetting = (key: string, value: unknown) =>
+ updateSetting("system", "main", key, value);
const updateAppSetting = (
appId: string,
category: string,
key: string,
value: unknown
- ) => {
- updateSetting(`${appId}.${category}`, key, value);
- };
+ ) => updateSetting(appId, category, key, value);
const getSystemSettings = () => {
return allSettings.system || {};
diff --git a/bridge-settings/react-app/src/services/websocketService.ts b/bridge-settings/react-app/src/services/websocketService.ts
index 0bc50eb..2c6a09b 100644
--- a/bridge-settings/react-app/src/services/websocketService.ts
+++ b/bridge-settings/react-app/src/services/websocketService.ts
@@ -118,9 +118,15 @@ export class WebSocketService {
}
}
- updateSetting(category: string, key: string, value: unknown): void {
+ updateSetting(
+ appId: string,
+ category: string,
+ key: string,
+ value: unknown
+ ): void {
this.send({
type: "updateSetting",
+ appId,
category,
key,
value,
@@ -134,7 +140,11 @@ export class WebSocketService {
});
}
- executeAction(appId: string, action: string, params: Record): void {
+ executeAction(
+ appId: string,
+ action: string,
+ params: Record
+ ): void {
this.send({
type: "executeAction",
appId,
diff --git a/bridge-settings/react-app/src/types/settings.ts b/bridge-settings/react-app/src/types/settings.ts
index 027690a..925956e 100644
--- a/bridge-settings/react-app/src/types/settings.ts
+++ b/bridge-settings/react-app/src/types/settings.ts
@@ -1,25 +1,56 @@
-// Message types for WebSocket communication with Kotlin backend
+export type SettingsMessage =
+ | {
+ type: "updateSetting";
+ appId: string;
+ category: string;
+ key: string;
+ value: unknown;
+ }
+ | { type: "registerForUpdates"; categories: string[] }
+ | { type: "getAllSettings" }
+ | {
+ type: "executeAction";
+ appId: string;
+ action: string;
+ params: Record;
+ };
-export type SettingsMessage =
- | { type: 'updateSetting'; category: string; key: string; value: unknown }
- | { type: 'registerForUpdates'; categories: string[] }
- | { type: 'getAllSettings' }
- | { type: 'executeAction'; appId: string; action: string; params: Record };
-
-export type StatusMessage =
- | { type: 'settingChanged'; category: string; key: string; value: unknown }
- | { type: 'statusUpdate'; statusType: string; data: Record }
- | { type: 'allSettings'; settings: Record> }
- | { type: 'appStatusUpdate'; appId: string; component: string; data: Record }
- | { type: 'appEvent'; appId: string; eventType: string; payload: Record }
- | { type: 'actionResult'; appId: string; action: string; success: boolean; message?: string; data?: Record; logs?: LogEntry[] }
- | { type: 'actionsRegistered'; appId: string; actions: Record }
- | { type: 'error'; message: string };
+export type StatusMessage =
+ | { type: "settingChanged"; category: string; key: string; value: unknown }
+ | { type: "statusUpdate"; statusType: string; data: Record }
+ | { type: "allSettings"; settings: Record> }
+ | {
+ type: "appStatusUpdate";
+ appId: string;
+ component: string;
+ data: Record;
+ }
+ | {
+ type: "appEvent";
+ appId: string;
+ eventType: string;
+ payload: Record;
+ }
+ | {
+ type: "actionResult";
+ appId: string;
+ action: string;
+ success: boolean;
+ message?: string;
+ data?: Record;
+ logs?: LogEntry[];
+ }
+ | {
+ type: "actionsRegistered";
+ appId: string;
+ actions: Record;
+ }
+ | { type: "error"; message: string };
export interface Setting {
key: string;
value: unknown;
- type: 'boolean' | 'integer' | 'string' | 'float' | 'action';
+ type: "boolean" | "integer" | "string" | "float" | "action";
defaultValue: unknown;
validation?: {
min?: number;
@@ -38,7 +69,7 @@ export interface ActionDefinition {
export interface ActionParameter {
name: string;
- type: 'boolean' | 'integer' | 'string' | 'float';
+ type: "boolean" | "integer" | "string" | "float";
required: boolean;
defaultValue?: unknown;
description?: string;
@@ -46,7 +77,7 @@ export interface ActionParameter {
export interface LogEntry {
timestamp: number;
- level: 'INFO' | 'WARNING' | 'ERROR' | 'DEBUG';
+ level: "INFO" | "WARNING" | "ERROR" | "DEBUG";
message: string;
}
@@ -98,4 +129,4 @@ export interface ConnectionState {
connected: boolean;
connecting: boolean;
error: string | null;
-}
\ No newline at end of file
+}
diff --git a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/EsimSettingsProvider.kt b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/EsimSettingsProvider.kt
index 615df10..ff4bad8 100644
--- a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/EsimSettingsProvider.kt
+++ b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/EsimSettingsProvider.kt
@@ -48,24 +48,24 @@ class EsimSettingsProvider(
}
}
- override fun getActionDefinitions(): Map {
+ override fun getActionDefinitions(): Map {
return mapOf(
- "getProfiles" to ActionDefinition(
+ "getProfiles" to LocalActionDefinition(
key = "getProfiles",
displayText = "List eSIM Profiles",
description = "Retrieve all eSIM profiles on the device"
),
- "getActiveProfile" to ActionDefinition(
+ "getActiveProfile" to LocalActionDefinition(
key = "getActiveProfile",
displayText = "Get Active Profile",
description = "Get the currently active eSIM profile"
),
- "getEid" to ActionDefinition(
+ "getEid" to LocalActionDefinition(
key = "getEid",
displayText = "Get Device EID",
description = "Retrieve the device's embedded identity document (EID)"
),
- "enableProfile" to ActionDefinition(
+ "enableProfile" to LocalActionDefinition(
key = "enableProfile",
displayText = "Enable Profile",
parameters = listOf(
@@ -78,7 +78,7 @@ class EsimSettingsProvider(
),
description = "Enable an eSIM profile by ICCID"
),
- "disableProfile" to ActionDefinition(
+ "disableProfile" to LocalActionDefinition(
key = "disableProfile",
displayText = "Disable Profile",
parameters = listOf(
@@ -91,7 +91,7 @@ class EsimSettingsProvider(
),
description = "Disable an eSIM profile by ICCID"
),
- "deleteProfile" to ActionDefinition(
+ "deleteProfile" to LocalActionDefinition(
key = "deleteProfile",
displayText = "Delete Profile",
parameters = listOf(
@@ -104,7 +104,7 @@ class EsimSettingsProvider(
),
description = "Permanently delete an eSIM profile"
),
- "setNickname" to ActionDefinition(
+ "setNickname" to LocalActionDefinition(
key = "setNickname",
displayText = "Set Profile Nickname",
parameters = listOf(
@@ -123,7 +123,7 @@ class EsimSettingsProvider(
),
description = "Set a custom nickname for an eSIM profile"
),
- "downloadProfile" to ActionDefinition(
+ "downloadProfile" to LocalActionDefinition(
key = "downloadProfile",
displayText = "Download Profile",
parameters = listOf(
@@ -136,7 +136,7 @@ class EsimSettingsProvider(
),
description = "Download a new eSIM profile from activation code"
),
- "downloadAndEnableProfile" to ActionDefinition(
+ "downloadAndEnableProfile" to LocalActionDefinition(
key = "downloadAndEnableProfile",
displayText = "Download & Enable Profile",
parameters = listOf(
diff --git a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsProvider.kt b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsProvider.kt
index 7cfa70d..bd2892d 100644
--- a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsProvider.kt
+++ b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsProvider.kt
@@ -123,8 +123,11 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
override fun sendAppStatusUpdate(appId: String, component: String, payload: Map<*, *>) {
providerScope.launch {
try {
- val convertedPayload = convertMapPayload(payload)
- settingsRegistry.sendAppStatusUpdate(appId, component, convertedPayload)
+ settingsRegistry.sendAppStatusUpdate(
+ appId,
+ component,
+ payload as Map
+ )
Log.d(TAG, "Sent app status update: $appId.$component")
} catch (e: Exception) {
Log.e(TAG, "Error sending app status update: $appId.$component", e)
@@ -135,8 +138,7 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
override fun sendAppEvent(appId: String, eventType: String, payload: Map<*, *>) {
providerScope.launch {
try {
- val convertedPayload = convertMapPayload(payload)
- settingsRegistry.sendAppEvent(appId, eventType, convertedPayload)
+ settingsRegistry.sendAppEvent(appId, eventType, payload as Map)
Log.d(TAG, "Sent app event: $appId.$eventType")
} catch (e: Exception) {
Log.e(TAG, "Error sending app event: $appId.$eventType", e)
@@ -147,8 +149,7 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
override fun executeAction(appId: String, action: String, params: Map<*, *>) {
providerScope.launch {
try {
- val convertedParams = convertMapPayload(params)
- settingsRegistry.executeAction(appId, action, convertedParams)
+ settingsRegistry.executeAction(appId, action, params as Map)
Log.i(TAG, "Executed action: $appId.$action")
} catch (e: Exception) {
Log.e(TAG, "Error executing action: $appId.$action", e)
@@ -156,19 +157,24 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
}
}
- override fun executeActionWithCallback(appId: String, action: String, params: Map<*, *>, callback: ISettingsCallback) {
+ override fun executeActionWithCallback(
+ appId: String,
+ action: String,
+ params: Map<*, *>,
+ callback: ISettingsCallback
+ ) {
providerScope.launch {
try {
- val convertedParams = convertMapPayload(params)
- val result = settingsRegistry.executeAction(appId, action, convertedParams)
+ val result =
+ settingsRegistry.executeAction(appId, action, params as Map)
Log.i(TAG, "Executed action with callback: $appId.$action")
-
+
safeCallback(TAG) {
callback.onActionResult(
- appId,
- action,
- result.success,
- result.message ?: "",
+ appId,
+ action,
+ result.success,
+ result.message ?: "",
result.data ?: emptyMap()
)
}
@@ -232,10 +238,6 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
}
}
- private fun convertMapPayload(payload: Map<*, *>): Map {
- return payload.mapKeys { it.key.toString() }.mapValues { it.value ?: "" }
- }
-
private fun notifySettingsChanged(allSettings: Map) {
// This would be implemented to notify specific callbacks about relevant changes
// For now, we'll skip detailed change detection
@@ -328,7 +330,7 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
}
}
}
-
+
com.penumbraos.bridge.types.AppActionInfo().apply {
this.appId = appId
this.actions = actionDefinitions
@@ -339,7 +341,7 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
emptyList()
}
}
-
+
private fun getSystemSettingDescription(key: String): String {
return when (key) {
"audio.volume" -> "Audio volume level (0-100)"
diff --git a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsRegistry.kt b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsRegistry.kt
index 5f3df27..f00c230 100644
--- a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsRegistry.kt
+++ b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsRegistry.kt
@@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
-import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
@@ -47,67 +46,14 @@ enum class LogLevel {
interface SettingsActionProvider {
suspend fun executeAction(action: String, params: Map): ActionResult
- fun getActionDefinitions(): Map
+ fun getActionDefinitions(): Map
}
-data class ActionDefinition(
- val key: String,
- val displayText: String,
- val parameters: List = emptyList(),
- val description: String? = null
-)
-
-data class ActionParameter(
- val name: String,
- val type: SettingType,
- val required: Boolean = true,
- val defaultValue: Any? = null,
- val description: String? = null
-)
-
-data class SettingDefinition(
- val key: String,
- val type: SettingType,
- val defaultValue: Any,
- val validation: SettingValidation? = null
-)
-
-enum class SettingType {
- BOOLEAN, INTEGER, STRING, FLOAT, ACTION
-}
-
-data class SettingValidation(
- val min: Number? = null,
- val max: Number? = null,
- val allowedValues: List? = null,
- val regex: String? = null
-)
-
-data class AppSettingsCategory(
- val appId: String,
- val category: String,
- val definitions: Map,
- val values: MutableMap = mutableMapOf()
-)
-
-@Serializable
-data class PersistedSettings(
- val systemSettings: Map = emptyMap(),
- val appSettings: Map>> = emptyMap()
-)
-
-data class ExecutingAction(
- val providerId: String,
- val actionName: String,
- val params: Map,
- val startTime: Long
-)
-
class SettingsRegistry(private val context: Context, val shellClient: ShellClient) {
private val appSettings = ConcurrentHashMap>()
private val systemSettings = ConcurrentHashMap()
private val actionProviders = ConcurrentHashMap()
-
+
// Execution state tracking
@Volatile
private var currentExecutingAction: ExecutingAction? = null
@@ -117,14 +63,14 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
companion object {
private const val EXECUTION_TIMEOUT_MS = 30000L // 30 seconds
}
-
+
private fun startExecutionTimeout(appId: String, action: String) {
clearState()
-
+
executionTimeoutRunnable = Runnable {
Log.w(TAG, "Action execution timeout: $appId.$action")
currentExecutingAction = null
-
+
// Broadcast timeout error
registryScope.launch {
sendAppEvent(
@@ -132,20 +78,22 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
"action" to action,
"success" to false,
"message" to "Action timed out after ${EXECUTION_TIMEOUT_MS / 1000} seconds",
- "logs" to listOf(mapOf(
- "timestamp" to System.currentTimeMillis(),
- "level" to "ERROR",
- "message" to "Action execution timeout"
- ))
+ "logs" to listOf(
+ mapOf(
+ "timestamp" to System.currentTimeMillis(),
+ "level" to "ERROR",
+ "message" to "Action execution timeout"
+ )
+ )
)
)
}
}
-
+
executionTimeoutHandler.postDelayed(executionTimeoutRunnable!!, EXECUTION_TIMEOUT_MS)
Log.d(TAG, "Started execution timeout for: $appId.$action")
}
-
+
private fun clearState() {
currentExecutingAction = null
executionTimeoutRunnable?.let {
@@ -165,8 +113,8 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
// Reference to web server for broadcasting (set by SettingsService)
private var webServer: SettingsWebServer? = null
- private val _settingsFlow = MutableStateFlow