@@ -10,8 +10,10 @@ import androidx.security.crypto.EncryptedSharedPreferences
10import androidx.security.crypto.MasterKey
10import androidx.security.crypto.MasterKey
11import soy.iko.opencode.data.model.ServerProfile
11import soy.iko.opencode.data.model.ServerProfile
12import soy.iko.opencode.data.network.OpencodeJson
12import soy.iko.opencode.data.network.OpencodeJson
13+import kotlinx.coroutines.Dispatchers
13import kotlinx.coroutines.flow.Flow
14import kotlinx.coroutines.flow.Flow
14import kotlinx.coroutines.flow.map
15import kotlinx.coroutines.flow.map
16+import kotlinx.coroutines.withContext
15import kotlinx.serialization.Serializable
17import kotlinx.serialization.Serializable
16import kotlinx.serialization.builtins.ListSerializer
18import kotlinx.serialization.builtins.ListSerializer
@@ -65,20 +67,25 @@ class ProfileStore(context: Context) {
65 .map { it.toProfile() }
67 .map { it.toProfile() }
68− private fun StoredProfile.toProfile() = ServerProfile(
70+ private suspend fun StoredProfile.toProfile(): ServerProfile {
73− password = username?.let { passwordFor(id) },
71+ val pw = username?.let { passwordFor(id) }
72+ return ServerProfile(
74− lastUsed = lastUsed,
78+ lastUsed = lastUsed,
77− /** Resolve a single profile with its password loaded, for opening a connection. */
78− suspend fun resolve(profile: ServerProfile): ServerProfile =
82+ suspend fun resolve(profile: ServerProfile): ServerProfile {
79− profile.copy(password = if (profile.hasAuth) passwordFor(profile.id) else null)
83+ val pw = if (profile.hasAuth) passwordFor(profile.id) else null
84+ return profile.copy(password = pw)
87+ private suspend fun passwordFor(id: String): String? =
81− fun passwordFor(id: String): String? = securePrefs.getString(passwordKey(id), null)
88+ withContext(Dispatchers.IO) { securePrefs.getString(passwordKey(id), null) }
83 suspend fun save(profile: ServerProfile) {
90 suspend fun save(profile: ServerProfile) {
84 appContext.dataStore.edit { prefs ->
91 appContext.dataStore.edit { prefs ->
@@ -97,14 +104,17 @@ class ProfileStore(context: Context) {
97 prefs[profilesKey] = OpencodeJson.encodeToString(ListSerializer(StoredProfile.serializer()), updated)
104 prefs[profilesKey] = OpencodeJson.encodeToString(ListSerializer(StoredProfile.serializer()), updated)
99 // Secret goes to encrypted storage (or is cleared when auth removed).
106 // Secret goes to encrypted storage (or is cleared when auth removed).
107+ // EncryptedSharedPreferences encrypts on the calling thread, so move to IO.
108+ withContext(Dispatchers.IO) {
100− securePrefs.edit().apply {
109+ securePrefs.edit().apply {
101− val pw = profile.password
110+ val pw = profile.password
102− if (!profile.username.isNullOrBlank() && !pw.isNullOrEmpty()) {
111+ if (!profile.username.isNullOrBlank() && !pw.isNullOrEmpty()) {
103− putString(passwordKey(profile.id), pw)
112+ putString(passwordKey(profile.id), pw)
104− } else {
113+ } else {
105− remove(passwordKey(profile.id))
114+ remove(passwordKey(profile.id))
107− }.apply()
116+ }.apply()
110 suspend fun delete(id: String) {
120 suspend fun delete(id: String) {
@@ -118,7 +128,9 @@ class ProfileStore(context: Context) {
118 current.filterNot { it.id == id },
128 current.filterNot { it.id == id },
131+ withContext(Dispatchers.IO) {
121− securePrefs.edit().remove(passwordKey(id)).apply()
132+ securePrefs.edit().remove(passwordKey(id)).apply()
124 private fun passwordKey(id: String) = "pw_$id"
136 private fun passwordKey(id: String) = "pw_$id"