git.iko.soy/opencode-android
on master help sign in
Move EncryptedSharedPreferences I/O off the main thread, make appScope private, and replace hardcoded leading spaces with proper Compose padding
commita626ac6ba7af7ba3ebc1ded634cc6d8774bfb1ea authoriko <mail@iko.soy> dateTue 30 Jun 2026, 04:07 (3 months ago) parent987f968
6 files changed+43−29
app/src/main/java/soy/iko/opencode/data/repo/ProfileStore.kt+33−21 app/src/main/java/soy/iko/opencode/di/AppContainer.kt+1−1 app/src/main/java/soy/iko/opencode/ui/chat/CommandPickerSheet.kt+2−1 app/src/main/java/soy/iko/opencode/ui/file/FileBrowserScreen.kt+4−3 app/src/main/java/soy/iko/opencode/ui/server/ServerListScreen.kt+1−1 app/src/main/java/soy/iko/opencode/ui/settings/SettingsScreen.kt+2−2
app/src/main/java/soy/iko/opencode/data/repo/ProfileStore.kt+33−21
@@ -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"
app/src/main/java/soy/iko/opencode/di/AppContainer.kt+1−1
@@ -42,7 +42,7 @@ import java.util.concurrent.atomic.AtomicBoolean
4242 */
4343class AppContainer(context: Context) {
4444
45− val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
45+ private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
4646 val profileStore = ProfileStore(context)
4747 val settingsStore = SettingsStore(context)
4848 val draftStore = DraftStore(context, appScope)
app/src/main/java/soy/iko/opencode/ui/chat/CommandPickerSheet.kt+2−1
@@ -59,8 +59,9 @@ fun CommandPickerSheet(
5959 ) {
6060 Icon(Icons.AutoMirrored.Filled.PlaylistPlay, contentDescription = null)
6161 Text(
62− " /${cmd.name}",
62+ "/${cmd.name}",
6363 style = MaterialTheme.typography.bodyLarge,
64+ modifier = Modifier.padding(start = 8.dp),
6465 )
6566 }
6667 Text(
app/src/main/java/soy/iko/opencode/ui/file/FileBrowserScreen.kt+4−3
@@ -229,11 +229,11 @@ private fun FileRow(
229229 tint = if (icon) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
230230 )
231231 Text(
232− " $label",
232+ label,
233233 style = MaterialTheme.typography.bodyLarge,
234234 maxLines = 1,
235235 overflow = TextOverflow.Ellipsis,
236− modifier = Modifier.weight(1f),
236+ modifier = Modifier.weight(1f).padding(start = 8.dp),
237237 )
238238 if (status != null) StatusBadge(status)
239239 }
@@ -261,10 +261,11 @@ private fun StatusBadge(status: FileStatusEntry) {
261261 )
262262 if (status.added > 0 || status.removed > 0) {
263263 Text(
264− " +${status.added} −${status.removed}",
264+ "+${status.added} −${status.removed}",
265265 style = MaterialTheme.typography.labelSmall,
266266 fontFamily = FontFamily.Monospace,
267267 color = MaterialTheme.colorScheme.onSurfaceVariant,
268+ modifier = Modifier.padding(start = 4.dp),
268269 )
269270 }
270271 }
app/src/main/java/soy/iko/opencode/ui/server/ServerListScreen.kt+1−1
@@ -217,7 +217,7 @@ private fun EmptyServers(onAdd: () -> Unit, modifier: Modifier = Modifier) {
217217 Spacer(Modifier.size(20.dp))
218218 TextButton(onClick = onAdd) {
219219 Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(18.dp))
220− Text(" " + stringResource(R.string.add_server))
220+ Text(stringResource(R.string.add_server), modifier = Modifier.padding(start = 8.dp))
221221 }
222222 }
223223 }
app/src/main/java/soy/iko/opencode/ui/settings/SettingsScreen.kt+2−2
@@ -147,7 +147,7 @@ fun SettingsScreen(container: AppContainer, onBack: () -> Unit, onManageServers:
147147 verticalAlignment = Alignment.CenterVertically,
148148 ) {
149149 Icon(Icons.Filled.Dns, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
150− Text(" " + stringResource(R.string.manage_servers), modifier = Modifier.weight(1f))
150+ Text(stringResource(R.string.manage_servers), modifier = Modifier.weight(1f).padding(start = 8.dp))
151151 Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
152152 }
153153
@@ -180,7 +180,7 @@ fun SettingsScreen(container: AppContainer, onBack: () -> Unit, onManageServers:
180180 verticalAlignment = Alignment.CenterVertically,
181181 ) {
182182 Icon(Icons.Filled.BugReport, contentDescription = null, tint = MaterialTheme.colorScheme.error)
183− Text(" " + stringResource(R.string.diagnostics), modifier = Modifier.weight(1f))
183+ Text(stringResource(R.string.diagnostics), modifier = Modifier.weight(1f).padding(start = 8.dp))
184184 Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
185185 }
186186 }