@@ -10,8 +10,10 @@ import androidx.security.crypto.EncryptedSharedPreferences
1010import androidx.security.crypto.MasterKey
1111import soy.iko.opencode.data.model.ServerProfile
1212import soy.iko.opencode.data.network.OpencodeJson
13+import kotlinx.coroutines.Dispatchers
1314import kotlinx.coroutines.flow.Flow
1415import kotlinx.coroutines.flow.map
16+import kotlinx.coroutines.withContext
1517import kotlinx.serialization.Serializable
1618import kotlinx.serialization.builtins.ListSerializer
1719
@@ -65,20 +67,25 @@ class ProfileStore(context: Context) {
6567 .map { it.toProfile() }
6668 }
6769
68− private fun StoredProfile.toProfile() = ServerProfile(
69− id = id,
70− label = label,
71− baseUrl = baseUrl,
72− username = username,
73− password = username?.let { passwordFor(id) },
74− lastUsed = lastUsed,
75− )
70+ private suspend fun StoredProfile.toProfile(): ServerProfile {
71+ val pw = username?.let { passwordFor(id) }
72+ return ServerProfile(
73+ id = id,
74+ label = label,
75+ baseUrl = baseUrl,
76+ username = username,
77+ password = pw,
78+ lastUsed = lastUsed,
79+ )
80+ }
7681
77− /** Resolve a single profile with its password loaded, for opening a connection. */
78− suspend fun resolve(profile: ServerProfile): ServerProfile =
79− profile.copy(password = if (profile.hasAuth) passwordFor(profile.id) else null)
82+ suspend fun resolve(profile: ServerProfile): ServerProfile {
83+ val pw = if (profile.hasAuth) passwordFor(profile.id) else null
84+ return profile.copy(password = pw)
85+ }
8086
81− fun passwordFor(id: String): String? = securePrefs.getString(passwordKey(id), null)
87+ private suspend fun passwordFor(id: String): String? =
88+ withContext(Dispatchers.IO) { securePrefs.getString(passwordKey(id), null) }
8289
8390 suspend fun save(profile: ServerProfile) {
8491 appContext.dataStore.edit { prefs ->
@@ -97,14 +104,17 @@ class ProfileStore(context: Context) {
97104 prefs[profilesKey] = OpencodeJson.encodeToString(ListSerializer(StoredProfile.serializer()), updated)
98105 }
99106 // Secret goes to encrypted storage (or is cleared when auth removed).
100− securePrefs.edit().apply {
101− val pw = profile.password
102− if (!profile.username.isNullOrBlank() && !pw.isNullOrEmpty()) {
103− putString(passwordKey(profile.id), pw)
104− } else {
105− remove(passwordKey(profile.id))
106− }
107− }.apply()
107+ // EncryptedSharedPreferences encrypts on the calling thread, so move to IO.
108+ withContext(Dispatchers.IO) {
109+ securePrefs.edit().apply {
110+ val pw = profile.password
111+ if (!profile.username.isNullOrBlank() && !pw.isNullOrEmpty()) {
112+ putString(passwordKey(profile.id), pw)
113+ } else {
114+ remove(passwordKey(profile.id))
115+ }
116+ }.apply()
117+ }
108118 }
109119
110120 suspend fun delete(id: String) {
@@ -118,7 +128,9 @@ class ProfileStore(context: Context) {
118128 current.filterNot { it.id == id },
119129 )
120130 }
121− securePrefs.edit().remove(passwordKey(id)).apply()
131+ withContext(Dispatchers.IO) {
132+ securePrefs.edit().remove(passwordKey(id)).apply()
133+ }
122134 }
123135
124136 private fun passwordKey(id: String) = "pw_$id"