ai-chat-android-auto / files / app / src / main / java / net / fuzzyworld / aichat / Prefs.kt
Prefs.kt
| 1 | package net.fuzzyworld.aichat |
| 2 | |
| 3 | import android.content.Context |
| 4 | import android.content.SharedPreferences |
| 5 | import androidx.security.crypto.EncryptedSharedPreferences |
| 6 | import androidx.security.crypto.MasterKey |
| 7 | import org.json.JSONArray |
| 8 | import org.json.JSONObject |
| 9 | |
| 10 | object Prefs { |
| 11 | private const val FILE = "ai_auto_prefs" |
| 12 | private const val KEY_PROVIDER = "provider_id" |
| 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, |
| 21 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, |
| 22 | ) |
| 23 | } |
| 24 | |
| 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 | |
| 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 | |
| 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 | } |
| 115 | } |