Skip to content

Commit c247f4d

Browse files
committed
Setting change listeners
1 parent 864e650 commit c247f4d

File tree

5 files changed

+249
-14
lines changed

5 files changed

+249
-14
lines changed

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

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,24 @@ import java.util.concurrent.ConcurrentHashMap
1414
private const val TAG = "SettingsProvider"
1515

1616
class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettingsProvider.Stub() {
17+
// Settings registrations with callbacks
18+
// TODO: Remove?
1719
private val callbacks = ConcurrentHashMap<String, ISettingsCallback>()
20+
21+
// Dynamic setting change listeners
22+
private val settingListeners = ConcurrentHashMap<String, MutableSet<SettingListener>>()
1823
private val providerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
1924

25+
private data class SettingListener(
26+
val callback: ISettingsCallback,
27+
val type: String
28+
)
29+
2030
init {
2131
// Monitor settings changes and notify callbacks
2232
providerScope.launch {
23-
settingsRegistry.settingsFlow.collect { allSettings ->
24-
notifySettingsChanged(allSettings)
33+
settingsRegistry.settingsFlow.collect { settingsUpdate ->
34+
notifySettingsChanged(settingsUpdate.changes)
2535
}
2636
}
2737
}
@@ -238,14 +248,79 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
238248
}
239249
}
240250

241-
private fun notifySettingsChanged(allSettings: Map<String, Any>) {
242-
// This would be implemented to notify specific callbacks about relevant changes
243-
// For now, we'll skip detailed change detection
251+
private fun notifySettingsChanged(changes: List<SettingChange>) {
252+
try {
253+
changes.forEach { change ->
254+
// Notify category-specific callbacks
255+
if (change.appId != "system") {
256+
val callback = callbacks["${change.appId}.${change.category}"]
257+
if (callback != null) {
258+
safeCallback(TAG) {
259+
Log.d(
260+
TAG,
261+
"Notifying category callback for: ${change.appId}.${change.category}.${change.key} = ${change.value}"
262+
)
263+
callback.onSettingChanged(
264+
change.appId,
265+
change.category,
266+
change.key,
267+
change.value!!.toString()
268+
)
269+
}
270+
}
271+
}
272+
273+
// Notify registered setting listeners
274+
val settingKey = "${change.appId}.${change.category}.${change.key}"
275+
settingListeners[settingKey]?.forEach { listener ->
276+
safeCallback(TAG) {
277+
Log.d(
278+
TAG,
279+
"Notifying ${listener.type} listener for: $settingKey = ${change.value}"
280+
)
281+
282+
listener.callback.onSettingChanged(
283+
change.appId,
284+
change.category,
285+
change.key,
286+
change.value!!.toString()
287+
)
288+
}
289+
}
290+
}
291+
} catch (e: Exception) {
292+
Log.e(TAG, "Error notifying settings changes", e)
293+
}
294+
}
295+
296+
private fun convertValueForListener(value: String, type: String): String {
297+
return try {
298+
when (type.lowercase()) {
299+
"boolean" -> {
300+
value.toBoolean().toString()
301+
}
302+
303+
"int", "integer" -> {
304+
value.toInt().toString()
305+
}
306+
307+
"float" -> {
308+
value.toFloat().toString()
309+
}
310+
311+
"string" -> value
312+
else -> value
313+
}
314+
} catch (e: Exception) {
315+
Log.w(TAG, "Failed to convert value '$value' to type '$type', using original", e)
316+
value
317+
}
244318
}
245319

246320
fun cleanup() {
247321
providerScope.cancel()
248322
callbacks.clear()
323+
settingListeners.clear()
249324
}
250325

251326
// Discovery methods for dynamic registration
@@ -351,4 +426,41 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
351426
else -> "System setting: $key"
352427
}
353428
}
429+
430+
override fun registerSettingListener(
431+
appId: String,
432+
category: String,
433+
key: String,
434+
type: String,
435+
callback: ISettingsCallback
436+
) {
437+
try {
438+
val settingKey = "$appId.$category.$key"
439+
val listener = SettingListener(callback, type)
440+
441+
settingListeners.getOrPut(settingKey) { mutableSetOf() }.add(listener)
442+
Log.i(TAG, "Registered $type listener for setting: $settingKey")
443+
} catch (e: Exception) {
444+
Log.e(TAG, "Failed to register listener for $appId.$category.$key", e)
445+
}
446+
}
447+
448+
override fun unregisterSettingListener(
449+
appId: String,
450+
category: String,
451+
key: String,
452+
callback: ISettingsCallback
453+
) {
454+
try {
455+
val settingKey = "$appId.$category.$key"
456+
settingListeners[settingKey]?.removeIf { it.callback == callback }
457+
458+
if (settingListeners[settingKey]?.isEmpty() == true) {
459+
settingListeners.remove(settingKey)
460+
}
461+
Log.i(TAG, "Unregistered listener for setting: $settingKey")
462+
} catch (e: Exception) {
463+
Log.e(TAG, "Failed to unregister listener for $appId.$category.$key", e)
464+
}
465+
}
354466
}

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

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ data class ActionResult(
3030
val logs: List<LogEntry>? = null
3131
)
3232

33+
data class SettingChange(
34+
val appId: String,
35+
val category: String,
36+
val key: String,
37+
val value: Any?,
38+
val previousValue: Any?
39+
)
40+
41+
data class SettingsUpdate(
42+
val allSettings: Map<String, Map<String, Any?>>,
43+
val changes: List<SettingChange>
44+
)
45+
3346
@Serializable
3447
data class LogEntry(
3548
val timestamp: Long = System.currentTimeMillis(),
@@ -112,8 +125,8 @@ class SettingsRegistry(
112125
// Reference to web server for broadcasting (set by SettingsService)
113126
private var webServer: SettingsWebServer? = null
114127

115-
private val _settingsFlow = MutableStateFlow<Map<String, Map<String, Any?>>>(emptyMap())
116-
val settingsFlow: StateFlow<Map<String, Map<String, Any?>>> = _settingsFlow.asStateFlow()
128+
private val _settingsFlow = MutableStateFlow<SettingsUpdate>(SettingsUpdate(emptyMap(), emptyList()))
129+
val settingsFlow: StateFlow<SettingsUpdate> = _settingsFlow.asStateFlow()
117130

118131
private val json = Json {
119132
prettyPrint = true
@@ -315,12 +328,14 @@ class SettingsRegistry(
315328
return false
316329
}
317330

331+
val previousValue = settingsCategory.values[key]
318332
settingsCategory.values[key] = value
319333

320334
// Save this specific app setting immediately
321335
saveAppSetting(appId, category, key, value)
322336

323-
updateSettingsFlow()
337+
val change = SettingChange(appId, category, key, value, previousValue)
338+
updateSettingsFlow(listOf(change))
324339
Log.i(TAG, "Updated app setting: $appId.$category.$key = $value")
325340
return true
326341
}
@@ -337,6 +352,8 @@ class SettingsRegistry(
337352

338353
suspend fun updateSystemSetting(key: String, value: Any): Boolean {
339354
if (validateSystemSetting(key, value)) {
355+
val previousValue = systemSettings[key]
356+
340357
// Apply the setting to Android system if it's a system setting
341358
val success = if (isAndroidSystemSetting(key)) {
342359
applyAndroidSystemSetting(key, value)
@@ -350,7 +367,8 @@ class SettingsRegistry(
350367
saveSystemSetting(key, value)
351368
}
352369

353-
updateSettingsFlow()
370+
val change = SettingChange("system", "", key, value, previousValue)
371+
updateSettingsFlow(listOf(change))
354372
Log.i(TAG, "Updated system setting: $key = $value")
355373
return true
356374
}
@@ -592,13 +610,16 @@ class SettingsRegistry(
592610
private fun setupTemperatureMonitoring() {
593611
registryScope.launch {
594612
temperatureController.temperatureFlow.collect { temperature ->
613+
val previousValue = systemSettings["device.temperature"]
595614
systemSettings["device.temperature"] = temperature
596-
updateSettingsFlow()
615+
val change = SettingChange("system", "", "device.temperature", temperature, previousValue)
616+
updateSettingsFlow(listOf(change))
597617
}
598618
}
599619
}
600620

601-
private fun updateSettingsFlow() {
602-
_settingsFlow.value = getAllSettings()
621+
private fun updateSettingsFlow(changes: List<SettingChange> = emptyList()) {
622+
val allSettings = getAllSettings()
623+
_settingsFlow.value = SettingsUpdate(allSettings, changes)
603624
}
604625
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ class SettingsWebServer(
153153

154154
// Monitor settings changes and broadcast to all clients
155155
serverScope.launch {
156-
settingsRegistry.settingsFlow.collect { allSettings ->
157-
broadcastSettingsUpdate(allSettings)
156+
settingsRegistry.settingsFlow.collect { settingsUpdate ->
157+
broadcastSettingsUpdate(settingsUpdate.allSettings)
158158
}
159159
}
160160

bridge-shared/aidl/com/penumbraos/bridge/ISettingsProvider.aidl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ interface ISettingsProvider {
1818
void sendAppStatusUpdate(String appId, String component, in Map payload);
1919
void sendAppEvent(String appId, String eventType, in Map payload);
2020

21+
void registerSettingListener(String appId, String category, String key, String type, ISettingsCallback callback);
22+
void unregisterSettingListener(String appId, String category, String key, ISettingsCallback callback);
23+
2124
// Discovery methods for dynamic registration
2225
List<SystemSettingInfo> getAvailableSystemSettings();
2326
List<String> getRegisteredApps();

sdk/src/main/java/com/penumbraos/sdk/api/SettingsClient.kt

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,25 @@ import kotlin.coroutines.resumeWithException
99

1010
private const val TAG = "SettingsClient"
1111

12+
interface BooleanSettingListener {
13+
fun onSettingChanged(value: Boolean)
14+
}
15+
16+
interface StringSettingListener {
17+
fun onSettingChanged(value: String)
18+
}
19+
20+
interface IntSettingListener {
21+
fun onSettingChanged(value: Int)
22+
}
23+
24+
interface FloatSettingListener {
25+
fun onSettingChanged(value: Float)
26+
}
27+
1228
@Suppress("UNCHECKED_CAST")
1329
class SettingsClient(private val settingsProvider: ISettingsProvider) {
30+
private val listenerCallbacks = mutableMapOf<String, ISettingsCallback>()
1431

1532
suspend fun registerSettings(
1633
appId: String,
@@ -140,6 +157,88 @@ class SettingsClient(private val settingsProvider: ISettingsProvider) {
140157
false
141158
}
142159
}
160+
161+
fun addBooleanListener(appId: String, category: String, key: String, listener: BooleanSettingListener) {
162+
val settingKey = "$appId.$category.$key"
163+
val callback = createTypedCallback(settingKey, "boolean") { value ->
164+
try {
165+
listener.onSettingChanged(value.toBoolean())
166+
} catch (e: Exception) {
167+
Log.e(TAG, "Failed to notify boolean listener for $settingKey", e)
168+
}
169+
}
170+
171+
listenerCallbacks[settingKey] = callback
172+
settingsProvider.registerSettingListener(appId, category, key, "boolean", callback)
173+
}
174+
175+
fun addStringListener(appId: String, category: String, key: String, listener: StringSettingListener) {
176+
val settingKey = "$appId.$category.$key"
177+
val callback = createTypedCallback(settingKey, "string") { value ->
178+
try {
179+
listener.onSettingChanged(value)
180+
} catch (e: Exception) {
181+
Log.e(TAG, "Failed to notify string listener for $settingKey", e)
182+
}
183+
}
184+
185+
listenerCallbacks[settingKey] = callback
186+
settingsProvider.registerSettingListener(appId, category, key, "string", callback)
187+
}
188+
189+
fun addIntListener(appId: String, category: String, key: String, listener: IntSettingListener) {
190+
val settingKey = "$appId.$category.$key"
191+
val callback = createTypedCallback(settingKey, "int") { value ->
192+
try {
193+
listener.onSettingChanged(value.toInt())
194+
} catch (e: Exception) {
195+
Log.e(TAG, "Failed to notify int listener for $settingKey", e)
196+
}
197+
}
198+
199+
listenerCallbacks[settingKey] = callback
200+
settingsProvider.registerSettingListener(appId, category, key, "int", callback)
201+
}
202+
203+
fun addFloatListener(appId: String, category: String, key: String, listener: FloatSettingListener) {
204+
val settingKey = "$appId.$category.$key"
205+
val callback = createTypedCallback(settingKey, "float") { value ->
206+
try {
207+
listener.onSettingChanged(value.toFloat())
208+
} catch (e: Exception) {
209+
Log.e(TAG, "Failed to notify float listener for $settingKey", e)
210+
}
211+
}
212+
213+
listenerCallbacks[settingKey] = callback
214+
settingsProvider.registerSettingListener(appId, category, key, "float", callback)
215+
}
216+
217+
fun removeListener(appId: String, category: String, key: String, listener: Any) {
218+
val settingKey = "$appId.$category.$key"
219+
val callback = listenerCallbacks.remove(settingKey)
220+
if (callback != null) {
221+
settingsProvider.unregisterSettingListener(appId, category, key, callback)
222+
}
223+
}
224+
225+
private fun createTypedCallback(settingKey: String, type: String, onChanged: (String) -> Unit): ISettingsCallback {
226+
return object : ISettingsCallback.Stub() {
227+
override fun onSettingChanged(appId: String, category: String, key: String, value: String) {
228+
onChanged(value)
229+
}
230+
231+
override fun onSettingsRegistered(appId: String, category: String) {
232+
}
233+
234+
override fun onError(message: String) {
235+
Log.e(TAG, "Listener callback error for $settingKey ($type): $message")
236+
}
237+
238+
override fun onActionResult(appId: String, action: String, success: Boolean, message: String, data: Map<*, *>) {
239+
}
240+
}
241+
}
143242
}
144243

145244
class SettingsCategoryBuilder {

0 commit comments

Comments
 (0)