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 f00c230..3e94cc4 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 @@ -1,7 +1,7 @@ package com.penumbraos.bridge_settings -import android.annotation.SuppressLint import android.content.Context +import android.content.SharedPreferences import android.media.AudioManager import android.os.Handler import android.os.Looper @@ -16,17 +16,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.booleanOrNull -import kotlinx.serialization.json.doubleOrNull -import kotlinx.serialization.json.intOrNull -import kotlinx.serialization.json.jsonPrimitive -import java.io.File import java.util.concurrent.ConcurrentHashMap private const val TAG = "SettingsRegistry" +private const val SYSTEM_SETTINGS_APP_ID = "penumbra_system" + data class ActionResult( val success: Boolean, val message: String? = null, @@ -49,9 +44,13 @@ interface SettingsActionProvider { fun getActionDefinitions(): Map } -class SettingsRegistry(private val context: Context, val shellClient: ShellClient) { +class SettingsRegistry( + private val context: Context, + private val sharedPreferences: SharedPreferences, + shellClient: ShellClient +) { private val appSettings = ConcurrentHashMap>() - private val systemSettings = ConcurrentHashMap() + private val systemSettings = ConcurrentHashMap() private val actionProviders = ConcurrentHashMap() // Execution state tracking @@ -102,9 +101,6 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien } } - // Store saved app settings values until apps register their schemas - private val savedAppSettingsValues = - ConcurrentHashMap>>() private val humaneDisplayController = HumaneDisplayController(shellClient) private val temperatureController = TemperatureController(shellClient) @@ -113,11 +109,9 @@ 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>>(emptyMap()) - val settingsFlow: StateFlow>> = _settingsFlow.asStateFlow() + private val _settingsFlow = MutableStateFlow>>(emptyMap()) + val settingsFlow: StateFlow>> = _settingsFlow.asStateFlow() - @SuppressLint("SdCardPath") - private val settingsFile = File("/sdcard/penumbra/etc/settings.json") private val json = Json { prettyPrint = true ignoreUnknownKeys = true @@ -142,30 +136,16 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien private fun loadSavedSettings() { try { - if (settingsFile.exists()) { - val persistedData = - json.decodeFromString(settingsFile.readText()) - - // Load non-Android system settings - persistedData.systemSettings.forEach { (key, value) -> + sharedPreferences.all.filter { it.key.startsWith(SYSTEM_SETTINGS_APP_ID) } + .forEach { (key, value) -> if (!isAndroidSystemSetting(key)) { - systemSettings[key] = value // Keep as string + systemSettings[key] = value } } - // Load app settings values (without schemas - will be merged when apps register) - persistedData.appSettings.forEach { (appId, categories) -> - val appSavedValues = - savedAppSettingsValues.getOrPut(appId) { ConcurrentHashMap() } - categories.forEach { (category, settingValues) -> - appSavedValues[category] = settingValues - } - } - - Log.i(TAG, "Loaded settings from ${settingsFile.absolutePath}") - } + Log.i(TAG, "Loaded settings from SharedPreferences") } catch (e: Exception) { - Log.w(TAG, "Failed to load settings from file", e) + Log.w(TAG, "Failed to load settings from SharedPreferences", e) } } @@ -288,26 +268,22 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien category: String, definitions: Map ) { + if (appId.startsWith(SYSTEM_SETTINGS_APP_ID)) { + throw IllegalArgumentException("Cannot register system settings") + } + Log.i(TAG, "Registering settings for app: $appId, category: $category") val appCategories = appSettings.getOrPut(appId) { mutableMapOf() } val settingsCategory = AppSettingsCategory(appId, category, definitions) - // Initialize with default values definitions.forEach { (key, definition) -> - settingsCategory.values[key] = definition.defaultValue - } - - // Merge in any previously saved values for this app/category - savedAppSettingsValues[appId]?.get(category)?.forEach { (key, jsonValue) -> - if (definitions.containsKey(key)) { - // Convert JsonElement back to proper type - val convertedValue = jsonValue.jsonPrimitive.let { primitive -> - primitive.booleanOrNull ?: primitive.intOrNull ?: primitive.doubleOrNull - ?: primitive.content - } - settingsCategory.values[key] = convertedValue - Log.d(TAG, "Restored saved value for $appId.$category.$key = $convertedValue") + val fullKey = "$appId.$category.$key" + val savedValue = sharedPreferences.all[fullKey] + if (savedValue != null) { + settingsCategory.values[key] = savedValue + } else { + settingsCategory.values[key] = definition.defaultValue } } @@ -336,8 +312,8 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien settingsCategory.values[key] = value - // Save app settings to file - saveSettings() + // Save this specific app setting immediately + saveAppSetting(appId, category, key, value) updateSettingsFlow() Log.i(TAG, "Updated app setting: $appId.$category.$key = $value") @@ -366,7 +342,7 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien if (success) { if (!isAndroidSystemSetting(key)) { systemSettings[key] = value - saveSettings() + saveSystemSetting(key, value) } updateSettingsFlow() @@ -381,12 +357,12 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien return systemSettings[key] } - fun getAllSystemSettings(): Map { + fun getAllSystemSettings(): Map { return systemSettings.toMap() } - fun getAllSettings(): Map> { - val result = mutableMapOf>() + fun getAllSettings(): Map> { + val result = mutableMapOf>() // Add system settings result["system"] = systemSettings.toMap() @@ -444,44 +420,40 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien } } - private fun saveSettings() { + private fun SharedPreferences.Editor.putValue(key: String, value: Any) { + when (value) { + is Boolean -> putBoolean(key, value) + is Int -> putInt(key, value) + is Long -> putLong(key, value) + is Float -> putFloat(key, value) + is String -> putString(key, value) + else -> putString(key, value.toString()) + } + } + + private fun saveSystemSetting(key: String, value: Any) { + val fullKey = "$SYSTEM_SETTINGS_APP_ID.$key" try { - // Create directory if it doesn't exist - settingsFile.parentFile?.mkdirs() - - // Collect non-Android system settings for persistence - val systemSettingsToSave = systemSettings - .filterKeys { !isAndroidSystemSetting(it) } - .mapValues { it.value.toString() } - - // Serialize app settings - val appSettingsToSave = mutableMapOf>>() - appSettings.forEach { (appId, categories) -> - val categoriesMap = mutableMapOf>() - categories.forEach { (category, categoryData) -> - val valuesMap = mutableMapOf() - categoryData.values.forEach { (key, value) -> - valuesMap[key] = when (value) { - is Boolean -> JsonPrimitive(value) - is Number -> JsonPrimitive(value) - is String -> JsonPrimitive(value) - else -> JsonPrimitive(value.toString()) - } - } - categoriesMap[category] = valuesMap - } - appSettingsToSave[appId] = categoriesMap + sharedPreferences.edit().apply { + putValue(fullKey, value) + apply() } - - val persistedData = PersistedSettings( - systemSettings = systemSettingsToSave, - appSettings = appSettingsToSave - ) - - settingsFile.writeText(json.encodeToString(persistedData)) - Log.d(TAG, "Settings saved to ${settingsFile.absolutePath}") + Log.d(TAG, "Saved system setting: $fullKey = $value") } catch (e: Exception) { - Log.e(TAG, "Failed to save settings to file", e) + Log.e(TAG, "Failed to save system setting $fullKey", e) + } + } + + private fun saveAppSetting(appId: String, category: String, key: String, value: Any) { + val fullKey = "$appId.$category.$key" + try { + sharedPreferences.edit().apply { + putValue(fullKey, value) + apply() + } + Log.d(TAG, "Saved app setting: $fullKey = $value") + } catch (e: Exception) { + Log.e(TAG, "Failed to save app setting $fullKey", e) } } diff --git a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsService.kt b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsService.kt index af736a9..74db12c 100644 --- a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsService.kt +++ b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsService.kt @@ -1,5 +1,6 @@ package com.penumbraos.bridge_settings +import android.content.SharedPreferences import android.util.Log import com.penumbraos.appprocessmocks.Common import com.penumbraos.appprocessmocks.MockContext @@ -14,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import java.io.File private const val TAG = "SettingsService" @@ -35,6 +37,22 @@ class SettingsService { val context = MockContext.createWithAppContext(classLoader, thread, "com.android.settings") + val settingsFile = File("/data/misc/user/0/penumbra/settings.xml") + settingsFile.mkdirs() + + val contextImplClass = classLoader.loadClass("android.app.ContextImpl") + val getSharedPreferencesByFileMethod = contextImplClass.getDeclaredMethod( + "getSharedPreferences", + File::class.java, Int::class.java + ) + getSharedPreferencesByFileMethod.isAccessible = true + + val sharedPreferences = + getSharedPreferencesByFileMethod.invoke( + context.baseContext, + settingsFile, 2 + ) as SharedPreferences + // Connect to bridge and get ShellClient val bridge = connectToBridge(TAG, context) Log.i(TAG, "Connected to bridge-core") @@ -46,7 +64,7 @@ class SettingsService { Log.i(TAG, "Created ShellClient") // Initialize components with context and shell client - settingsRegistry = SettingsRegistry(context, shellClient) + settingsRegistry = SettingsRegistry(context, sharedPreferences, shellClient) settingsRegistry.initialize() settingsProvider = SettingsProvider(settingsRegistry) diff --git a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsWebServer.kt b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsWebServer.kt index 0aee002..702a1d1 100644 --- a/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsWebServer.kt +++ b/bridge-settings/src/main/java/com/penumbraos/bridge_settings/SettingsWebServer.kt @@ -424,7 +424,7 @@ class SettingsWebServer( } } - private suspend fun broadcastSettingsUpdate(allSettings: Map>) { + private suspend fun broadcastSettingsUpdate(allSettings: Map>) { val message = StatusMessage.AllSettings(allSettings.toJsonElement()) broadcast(message) } @@ -443,7 +443,7 @@ class SettingsWebServer( Log.d(TAG, "Broadcasted app status update: $appId.$component") } - suspend fun broadcastAppEvent(appId: String, eventType: String, payload: Map) { + suspend fun broadcastAppEvent(appId: String, eventType: String, payload: Map) { val message = StatusMessage.AppEvent( appId = appId, eventType = eventType, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b2e12b..c7691d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ robolectric = "4.14" okhttp = "4.12.0" ktor = "3.0.0" systemJars = "70382bc" -appprocessmocks = "2af582c" +appprocessmocks = "9f2a20a" dnsjava = "3.6.3" [libraries]