ai-chat-android-auto / files / app / src / main / java / net / fuzzyworld / aichat / Prefs.kt
Prefs.kt
d522fe6d 2026-05-15 Pavel Safronov | 1 | package net.fuzzyworld.aichat |
80eca30b 2026-05-14 Pavel Safronov | 2 | |
| 3 | import android.content.Context | |
| 4 | import android.content.SharedPreferences | |
| 5 | import androidx.security.crypto.EncryptedSharedPreferences | |
| 6 | import androidx.security.crypto.MasterKey | |
449ce939 2026-05-14 Pavel Safronov | 7 | import org.json.JSONArray |
| 8 | import org.json.JSONObject | |
80eca30b 2026-05-14 Pavel Safronov | 9 | |
| 10 | object Prefs { | |
449ce939 2026-05-14 Pavel Safronov | 11 | private const val FILE = "ai_auto_prefs" |
| 12 | private const val KEY_PROVIDER = "provider_id" | |
80eca30b 2026-05-14 Pavel Safronov | 13 | |
| 14 | private fun prefs(context: Context): SharedPreferences { | |
| 15 | val master = MasterKey.Builder(context) | |
| 16 | .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) | |
| 17 | .build() | |
| 18 | return EncryptedSharedPreferences.create( | |
| 19 | context, FILE, master, | |
| 20 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, | |
449ce939 2026-05-14 Pavel Safronov | 21 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, |
80eca30b 2026-05-14 Pavel Safronov | 22 | ) |
| 23 | } | |
| 24 | ||
449ce939 2026-05-14 Pavel Safronov | 25 | // ── Provider selection ───────────────────────────────────────────────────── |
| 26 | ||
| 27 | fun selectedProviderId(context: Context): String = | |
| 28 | prefs(context).getString(KEY_PROVIDER, AnthropicProvider.id) ?: AnthropicProvider.id | |
| 29 | ||
| 30 | fun saveProviderId(context: Context, id: String) = | |
| 31 | prefs(context).edit().putString(KEY_PROVIDER, id).apply() | |
| 32 | ||
| 33 | // ── Per-provider API key ─────────────────────────────────────────────────── | |
| 34 | ||
| 35 | fun apiKey(context: Context, providerId: String = selectedProviderId(context)): String = | |
| 36 | prefs(context).getString("key_$providerId", "") ?: "" | |
| 37 | ||
| 38 | fun hasApiKey(context: Context, providerId: String = selectedProviderId(context)): Boolean = | |
| 39 | apiKey(context, providerId).isNotBlank() | |
| 40 | ||
| 41 | fun saveApiKey(context: Context, providerId: String, key: String) = | |
| 42 | prefs(context).edit().putString("key_$providerId", key).apply() | |
| 43 | ||
| 44 | fun clearApiKey(context: Context, providerId: String) = | |
| 45 | prefs(context).edit().remove("key_$providerId").apply() | |
| 46 | ||
| 47 | // ── Per-provider base URL (with fallback to provider default) ────────────── | |
| 48 | ||
| 49 | fun baseUrl(context: Context, providerId: String = selectedProviderId(context)): String { | |
| 50 | val default = ProviderRegistry.byId(providerId).defaultBaseUrl | |
| 51 | return prefs(context).getString("url_$providerId", default)?.ifBlank { default } ?: default | |
| 52 | } | |
| 53 | ||
| 54 | fun saveBaseUrl(context: Context, providerId: String, url: String) = | |
| 55 | prefs(context).edit().putString("url_$providerId", url).apply() | |
| 56 | ||
| 57 | // ── Per-provider selected model (with fallback to provider default) ───────── | |
| 58 | ||
| 59 | fun model(context: Context, providerId: String = selectedProviderId(context)): String { | |
| 60 | val default = ProviderRegistry.byId(providerId).defaultModel | |
| 61 | return prefs(context).getString("model_$providerId", default)?.ifBlank { default } ?: default | |
| 62 | } | |
| 63 | ||
| 64 | fun saveModel(context: Context, providerId: String, modelId: String) = | |
| 65 | prefs(context).edit().putString("model_$providerId", modelId).apply() | |
| 66 | ||
| 67 | // ── Cached model list ────────────────────────────────────────────────────── | |
| 68 | ||
| 69 | fun cachedModels(context: Context, providerId: String): List<ModelInfo>? { | |
| 70 | val json = prefs(context).getString("models_$providerId", null) ?: return null | |
| 71 | return try { | |
| 72 | val arr = JSONArray(json) | |
| 73 | (0 until arr.length()).map { i -> | |
| 74 | val obj = arr.getJSONObject(i) | |
| 75 | ModelInfo(obj.getString("id"), obj.getString("displayName")) | |
| 76 | } | |
| 77 | } catch (_: Exception) { null } | |
| 78 | } | |
| 79 | ||
| 80 | fun saveModelCache(context: Context, providerId: String, models: List<ModelInfo>) { | |
| 81 | val arr = JSONArray().apply { | |
| 82 | models.forEach { m -> | |
| 83 | put(JSONObject().apply { | |
| 84 | put("id", m.id) | |
| 85 | put("displayName", m.displayName) | |
| 86 | }) | |
| 87 | } | |
| 88 | } | |
| 89 | prefs(context).edit().putString("models_$providerId", arr.toString()).apply() | |
| 90 | } | |
| 91 | ||
9090d7c8 2026-05-15 Pavel Safronov | 92 | // ── Voice output settings ────────────────────────────────────────────────── |
| 93 | ||
| 94 | fun voiceId(context: Context): String = | |
| 95 | prefs(context).getString("voice_id", "") ?: "" | |
| 96 | ||
| 97 | fun saveVoiceId(context: Context, id: String) = | |
| 98 | prefs(context).edit().putString("voice_id", id).apply() | |
| 99 | ||
| 100 | fun speechRate(context: Context): Float = | |
| 101 | prefs(context).getFloat("speech_rate", 1.0f) | |
| 102 | ||
| 103 | fun saveSpeechRate(context: Context, rate: Float) = | |
| 104 | prefs(context).edit().putFloat("speech_rate", rate).apply() | |
| 105 | ||
449ce939 2026-05-14 Pavel Safronov | 106 | // ── Ready check (used by the media service) ──────────────────────────────── |
| 107 | ||
| 108 | fun isReady(context: Context): Boolean { | |
| 109 | val providerId = selectedProviderId(context) | |
| 110 | val provider = ProviderRegistry.byId(providerId) | |
| 111 | if (provider.requiresApiKey && !hasApiKey(context, providerId)) return false | |
| 112 | if (provider.allowsBaseUrlOverride && baseUrl(context, providerId).isBlank()) return false | |
| 113 | return model(context, providerId).isNotBlank() | |
| 114 | } | |
80eca30b 2026-05-14 Pavel Safronov | 115 | } |