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
a626ac6iko
app/src/main/java/soy/iko/opencode/data/repo/ProfileStore.kt+33−21
@@ -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
17
19
@@ -65,20 +67,25 @@ class ProfileStore(context: Context) {
65 .map { it.toProfile() }
67 .map { it.toProfile() }
66 }
68 }
67
69
68− private fun StoredProfile.toProfile() = ServerProfile(
70+ private suspend fun StoredProfile.toProfile(): ServerProfile {
69− id = id,
70− label = label,
71− baseUrl = baseUrl,
72− username = username,
73− password = username?.let { passwordFor(id) },
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,
74− lastUsed = lastUsed,
78+ lastUsed = lastUsed,
75− )
79+ )
80+ }
76
81
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)
85+ }
80
86
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) }
82
89
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)
98 }
105 }
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))
106− }
115+ }
107− }.apply()
116+ }.apply()
117+ }
108 }
118 }
109
119
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 },
119 )
129 )
120 }
130 }
131+ withContext(Dispatchers.IO) {
121− securePrefs.edit().remove(passwordKey(id)).apply()
132+ securePrefs.edit().remove(passwordKey(id)).apply()
133+ }
122 }
134 }
123
135
124 private fun passwordKey(id: String) = "pw_$id"
136 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
42 */
42 */
43class AppContainer(context: Context) {
43class AppContainer(context: Context) {
44
44
45− val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
45+ private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
46 val profileStore = ProfileStore(context)
46 val profileStore = ProfileStore(context)
47 val settingsStore = SettingsStore(context)
47 val settingsStore = SettingsStore(context)
48 val draftStore = DraftStore(context, appScope)
48 val draftStore = DraftStore(context, appScope)
app/src/main/java/soy/iko/opencode/ui/chat/CommandPickerSheet.kt+2−1
@@ -59,8 +59,9 @@ fun CommandPickerSheet(
59 ) {
59 ) {
60 Icon(Icons.AutoMirrored.Filled.PlaylistPlay, contentDescription = null)
60 Icon(Icons.AutoMirrored.Filled.PlaylistPlay, contentDescription = null)
61 Text(
61 Text(
62− " /${cmd.name}",
62+ "/${cmd.name}",
63 style = MaterialTheme.typography.bodyLarge,
63 style = MaterialTheme.typography.bodyLarge,
64+ modifier = Modifier.padding(start = 8.dp),
64 )
65 )
65 }
66 }
66 Text(
67 Text(
app/src/main/java/soy/iko/opencode/ui/file/FileBrowserScreen.kt+4−3
@@ -229,11 +229,11 @@ private fun FileRow(
229 tint = if (icon) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
229 tint = if (icon) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
230 )
230 )
231 Text(
231 Text(
232− " $label",
232+ label,
233 style = MaterialTheme.typography.bodyLarge,
233 style = MaterialTheme.typography.bodyLarge,
234 maxLines = 1,
234 maxLines = 1,
235 overflow = TextOverflow.Ellipsis,
235 overflow = TextOverflow.Ellipsis,
236− modifier = Modifier.weight(1f),
236+ modifier = Modifier.weight(1f).padding(start = 8.dp),
237 )
237 )
238 if (status != null) StatusBadge(status)
238 if (status != null) StatusBadge(status)
239 }
239 }
@@ -261,10 +261,11 @@ private fun StatusBadge(status: FileStatusEntry) {
261 )
261 )
262 if (status.added > 0 || status.removed > 0) {
262 if (status.added > 0 || status.removed > 0) {
263 Text(
263 Text(
264− " +${status.added} −${status.removed}",
264+ "+${status.added} −${status.removed}",
265 style = MaterialTheme.typography.labelSmall,
265 style = MaterialTheme.typography.labelSmall,
266 fontFamily = FontFamily.Monospace,
266 fontFamily = FontFamily.Monospace,
267 color = MaterialTheme.colorScheme.onSurfaceVariant,
267 color = MaterialTheme.colorScheme.onSurfaceVariant,
268+ modifier = Modifier.padding(start = 4.dp),
268 )
269 )
269 }
270 }
270 }
271 }
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) {
217 Spacer(Modifier.size(20.dp))
217 Spacer(Modifier.size(20.dp))
218 TextButton(onClick = onAdd) {
218 TextButton(onClick = onAdd) {
219 Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(18.dp))
219 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))
221 }
221 }
222 }
222 }
223 }
223 }
app/src/main/java/soy/iko/opencode/ui/settings/SettingsScreen.kt+2−2
@@ -147,7 +147,7 @@ fun SettingsScreen(container: AppContainer, onBack: () -> Unit, onManageServers:
147 verticalAlignment = Alignment.CenterVertically,
147 verticalAlignment = Alignment.CenterVertically,
148 ) {
148 ) {
149 Icon(Icons.Filled.Dns, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
149 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))
151 Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
151 Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
152 }
152 }
153
153
@@ -180,7 +180,7 @@ fun SettingsScreen(container: AppContainer, onBack: () -> Unit, onManageServers:
180 verticalAlignment = Alignment.CenterVertically,
180 verticalAlignment = Alignment.CenterVertically,
181 ) {
181 ) {
182 Icon(Icons.Filled.BugReport, contentDescription = null, tint = MaterialTheme.colorScheme.error)
182 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))
184 Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
184 Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
185 }
185 }
186 }
186 }