@@ -8,6 +8,8 @@ import androidx.datastore.preferences.core.*
8import androidx.datastore.preferences.preferencesDataStore
8import androidx.datastore.preferences.preferencesDataStore
9import androidx.security.crypto.EncryptedSharedPreferences
9import androidx.security.crypto.EncryptedSharedPreferences
10import androidx.security.crypto.MasterKey
10import androidx.security.crypto.MasterKey
11+import com.parseable.android.data.local.SavedServer
12+import com.parseable.android.data.local.SavedServerDao
11import com.parseable.android.data.model.ServerConfig
13import com.parseable.android.data.model.ServerConfig
12import dagger.hilt.android.qualifiers.ApplicationContext
14import dagger.hilt.android.qualifiers.ApplicationContext
13import kotlinx.coroutines.Dispatchers
15import kotlinx.coroutines.Dispatchers
@@ -27,10 +29,12 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
28class SettingsRepository @Inject constructor(
30class SettingsRepository @Inject constructor(
29 @ApplicationContext private val context: Context,
31 @ApplicationContext private val context: Context,
32+ private val savedServerDao: SavedServerDao,
31 private val serverUrlKey = stringPreferencesKey("server_url")
34 private val serverUrlKey = stringPreferencesKey("server_url")
32 private val usernameKey = stringPreferencesKey("username")
35 private val usernameKey = stringPreferencesKey("username")
33 private val useTlsKey = booleanPreferencesKey("use_tls")
36 private val useTlsKey = booleanPreferencesKey("use_tls")
37+ private val activeServerIdKey = longPreferencesKey("active_server_id")
35 /** Guards concurrent reads/writes across DataStore and EncryptedSharedPreferences. */
39 /** Guards concurrent reads/writes across DataStore and EncryptedSharedPreferences. */
36 private val configMutex = Mutex()
40 private val configMutex = Mutex()
@@ -62,6 +66,8 @@ class SettingsRepository @Inject constructor(
69+ // ── Active server config (current connection) ──────────────────────
65 val serverConfig: Flow<ServerConfig?> = context.dataStore.data
71 val serverConfig: Flow<ServerConfig?> = context.dataStore.data
66 .catch { e ->
72 .catch { e ->
67 Timber.e(e, "Failed to read DataStore")
73 Timber.e(e, "Failed to read DataStore")
@@ -87,6 +93,14 @@ class SettingsRepository @Inject constructor(
88 .flowOn(Dispatchers.IO)
94 .flowOn(Dispatchers.IO)
96+ val activeServerId: Flow<Long?> = context.dataStore.data
98+ Timber.e(e, "Failed to read DataStore")
99+ emit(emptyPreferences())
101+ .map { prefs -> prefs[activeServerIdKey] }
102+ .flowOn(Dispatchers.IO)
90 suspend fun getSavedPassword(): ServerConfig? = serverConfig.first()
104 suspend fun getSavedPassword(): ServerConfig? = serverConfig.first()
92 suspend fun saveServerConfig(config: ServerConfig) {
106 suspend fun saveServerConfig(config: ServerConfig) {
@@ -109,13 +123,111 @@ class SettingsRepository @Inject constructor(
109 suspend fun clearConfig() {
123 suspend fun clearConfig() {
110 configMutex.withLock {
124 configMutex.withLock {
112− context.dataStore.edit { it.clear() }
126+ context.dataStore.edit { prefs ->
127+ prefs.remove(serverUrlKey)
128+ prefs.remove(usernameKey)
129+ prefs.remove(useTlsKey)
130+ prefs.remove(activeServerIdKey)
113 withContext(Dispatchers.IO) {
132 withContext(Dispatchers.IO) {
114− encryptedPrefs.edit().clear().apply()
133+ encryptedPrefs.edit().remove("password").apply()
116 } catch (e: Exception) {
135 } catch (e: Exception) {
117 Timber.e(e, "Failed to clear config")
136 Timber.e(e, "Failed to clear config")
141+ // ── Multi-server management ────────────────────────────────────────
143+ val savedServers: Flow<List<SavedServer>> = savedServerDao.getAll()
146+ * Persist the current server connection as a saved server entry.
147+ * If a server with the same URL and username already exists, updates it.
148+ * Returns the server ID.
150+ suspend fun saveServer(config: ServerConfig): Long {
151+ return configMutex.withLock {
152+ val existing = savedServerDao.findByUrlAndUsername(config.serverUrl, config.username)
153+ val passwordKey = existing?.passwordKey ?: "server_pwd_${System.currentTimeMillis()}"
154+ val server = SavedServer(
155+ id = existing?.id ?: 0,
156+ serverUrl = config.serverUrl,
157+ username = config.username,
158+ useTls = config.useTls,
159+ passwordKey = passwordKey,
160+ addedAt = existing?.addedAt ?: System.currentTimeMillis(),
162+ withContext(Dispatchers.IO) {
163+ encryptedPrefs.edit().putString(passwordKey, config.password).apply()
165+ val id = savedServerDao.insert(server)
167+ // Update active server ID
168+ context.dataStore.edit { prefs ->
169+ prefs[activeServerIdKey] = id
176+ * Switch to a previously saved server. Loads its credentials into the
177+ * active config (DataStore + encrypted password).
179+ suspend fun switchToServer(serverId: Long): ServerConfig? {
180+ return configMutex.withLock {
181+ val server = savedServerDao.getById(serverId) ?: return@withLock null
182+ val password = withContext(Dispatchers.IO) {
184+ encryptedPrefs.getString(server.passwordKey, null)
185+ } catch (e: Exception) {
186+ Timber.e(e, "Failed to read server password")
189+ } ?: return@withLock null
191+ val config = ServerConfig(
192+ serverUrl = server.serverUrl,
193+ username = server.username,
194+ password = password,
195+ useTls = server.useTls,
198+ // Write to active config
199+ context.dataStore.edit { prefs ->
200+ prefs[serverUrlKey] = config.serverUrl
201+ prefs[usernameKey] = config.username
202+ prefs[useTlsKey] = config.useTls
203+ prefs[activeServerIdKey] = serverId
205+ withContext(Dispatchers.IO) {
206+ encryptedPrefs.edit().putString("password", config.password).apply()
213+ * Remove a saved server and its stored password.
214+ * If the deleted server was the active one, the active server ID is cleared.
216+ suspend fun deleteServer(serverId: Long) {
217+ configMutex.withLock {
218+ val server = savedServerDao.getById(serverId) ?: return@withLock
219+ withContext(Dispatchers.IO) {
220+ encryptedPrefs.edit().remove(server.passwordKey).apply()
222+ savedServerDao.deleteById(serverId)
224+ // Clear active server reference if it pointed at the deleted server
225+ val currentActiveId = context.dataStore.data.first()[activeServerIdKey]
226+ if (currentActiveId == serverId) {
227+ context.dataStore.edit { prefs ->
228+ prefs.remove(activeServerIdKey)