git.iko.soy/opencode-android
on master help sign in
Fix lint warnings, preview flash, and refresh race in session list
cea6f43iko
app/src/main/AndroidManifest.xml+3−1
@@ -1,5 +1,6 @@
1<?xml version="1.0" encoding="utf-8"?>
1<?xml version="1.0" encoding="utf-8"?>
2−<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+ xmlns:tools="http://schemas.android.com/tools">
3
4
4 <uses-permission android:name="android.permission.INTERNET" />
5 <uses-permission android:name="android.permission.INTERNET" />
5 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -24,6 +25,7 @@
24 android:launchMode="singleTop"
25 android:launchMode="singleTop"
25 android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden|uiMode|navigation"
26 android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden|uiMode|navigation"
26 android:enableOnBackInvokedCallback="true"
27 android:enableOnBackInvokedCallback="true"
28+ tools:targetApi="33"
27 android:theme="@style/Theme.OpencodeAndroid.Splash">
29 android:theme="@style/Theme.OpencodeAndroid.Splash">
28 <intent-filter>
30 <intent-filter>
29 <action android:name="android.intent.action.MAIN" />
31 <action android:name="android.intent.action.MAIN" />
app/src/main/java/soy/iko/opencode/notification/NotificationChannels.kt−2
@@ -3,7 +3,6 @@ package soy.iko.opencode.notification
3import android.app.NotificationChannel
3import android.app.NotificationChannel
4import android.app.NotificationManager
4import android.app.NotificationManager
5import android.content.Context
5import android.content.Context
6−import android.os.Build
7import soy.iko.opencode.R
6import soy.iko.opencode.R
8
7
9/**
8/**
@@ -19,7 +18,6 @@ object NotificationChannels {
19 const val COMPLETED = "session_completed"
18 const val COMPLETED = "session_completed"
20
19
21 fun create(context: Context) {
20 fun create(context: Context) {
22− if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
23 val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager ?: return
21 val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager ?: return
24 nm.createNotificationChannel(
22 nm.createNotificationChannel(
25 NotificationChannel(STATUS, context.getString(R.string.notif_channel_status), NotificationManager.IMPORTANCE_LOW).apply {
23 NotificationChannel(STATUS, context.getString(R.string.notif_channel_status), NotificationManager.IMPORTANCE_LOW).apply {
app/src/main/java/soy/iko/opencode/notification/RunForegroundService.kt+1−6
@@ -72,12 +72,7 @@ class RunForegroundService : Service() {
72 private const val NOTIF_ID = 1
72 private const val NOTIF_ID = 1
73
73
74 fun start(context: Context) {
74 fun start(context: Context) {
75− val intent = Intent(context, RunForegroundService::class.java)
75+ context.startForegroundService(Intent(context, RunForegroundService::class.java))
76− if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
77− context.startForegroundService(intent)
78− } else {
79− context.startService(intent)
80− }
81 }
76 }
82
77
83 fun stop(context: Context) {
78 fun stop(context: Context) {
app/src/main/java/soy/iko/opencode/ui/session/SessionListViewModel.kt+7−2
@@ -90,6 +90,7 @@ class SessionListViewModel(private val container: AppContainer) : ViewModel() {
90 fun clearTransientError() { _transientError.value = null }
90 fun clearTransientError() { _transientError.value = null }
91
91
92 private var previewJob: Job? = null
92 private var previewJob: Job? = null
93+ private var refreshJob: Job? = null
93
94
94 /** Per-session in-flight preview loads, so rapid SSE events for the same session
95 /** Per-session in-flight preview loads, so rapid SSE events for the same session
95 * coalesce into a single request instead of firing one per event. Accessed from
96 * coalesce into a single request instead of firing one per event. Accessed from
@@ -166,7 +167,8 @@ class SessionListViewModel(private val container: AppContainer) : ViewModel() {
166 _serverLabel.value = conn.profile.displayLabel
167 _serverLabel.value = conn.profile.displayLabel
167 _state.update { it.copy(loading = true, error = null) }
168 _state.update { it.copy(loading = true, error = null) }
168 _refreshing.value = true
169 _refreshing.value = true
170+ refreshJob?.cancel()
169− viewModelScope.launch {
171+ refreshJob = viewModelScope.launch {
170 runCatching { conn.repository.listSessions() }
172 runCatching { conn.repository.listSessions() }
171 .onSuccess { list ->
173 .onSuccess { list ->
172 val sorted = list.sortedByDescending { it.time?.updated ?: it.time?.created ?: 0 }
174 val sorted = list.sortedByDescending { it.time?.updated ?: it.time?.created ?: 0 }
@@ -212,7 +214,10 @@ class SessionListViewModel(private val container: AppContainer) : ViewModel() {
212 previewJob?.cancel()
214 previewJob?.cancel()
213 val conn = container.activeConnection.value ?: return
215 val conn = container.activeConnection.value ?: return
214 val api = conn.api
216 val api = conn.api
217+ // Keep previews for sessions still in the list; drop stale ones without
218+ // clearing the whole map (which would cause a visual flash).
219+ val keepIds = sessions.mapTo(mutableSetOf()) { it.id }
215− _state.update { it.copy(previews = emptyMap()) }
220+ _state.update { s -> s.copy(previews = s.previews.filterKeys { it in keepIds }) }
216 previewJob = viewModelScope.launch {
221 previewJob = viewModelScope.launch {
217 val semaphore = Semaphore(NetworkConfig.maxConcurrentPreviews)
222 val semaphore = Semaphore(NetworkConfig.maxConcurrentPreviews)
218 sessions.take(NetworkConfig.maxPreviewSessions).map { session ->
223 sessions.take(NetworkConfig.maxPreviewSessions).map { session ->
app/src/main/res/values/strings.xml−7
@@ -5,7 +5,6 @@
5
5
6 <!-- Common -->
6 <!-- Common -->
7 <string name="back">Back</string>
7 <string name="back">Back</string>
8− <string name="loading">Loading</string>
9 <string name="cancel">Cancel</string>
8 <string name="cancel">Cancel</string>
10 <string name="save">Save</string>
9 <string name="save">Save</string>
11 <string name="delete">Delete</string>
10 <string name="delete">Delete</string>
@@ -89,7 +88,6 @@
89 <!-- Errors (view models) -->
88 <!-- Errors (view models) -->
90 <string name="error_generic">Something went wrong.</string>
89 <string name="error_generic">Something went wrong.</string>
91 <string name="error_client_status">Request failed (%1$d).</string>
90 <string name="error_client_status">Request failed (%1$d).</string>
92− <string name="error_failed_to_send">Failed to send</string>
93 <string name="error_agent_reported">The agent reported an error.</string>
91 <string name="error_agent_reported">The agent reported an error.</string>
94 <string name="error_not_reachable">Could not reach %1$s</string>
92 <string name="error_not_reachable">Could not reach %1$s</string>
95 <string name="error_timeout">The request timed out. Check the connection and try again.</string>
93 <string name="error_timeout">The request timed out. Check the connection and try again.</string>
@@ -102,7 +100,6 @@
102 <string name="show_less">Show less</string>
100 <string name="show_less">Show less</string>
103 <string name="tool_completed">Completed</string>
101 <string name="tool_completed">Completed</string>
104 <string name="tool_error">Error</string>
102 <string name="tool_error">Error</string>
105− <string name="tool_running">In progress</string>
106 <string name="state_expanded">Expanded</string>
103 <string name="state_expanded">Expanded</string>
107 <string name="state_collapsed">Collapsed</string>
104 <string name="state_collapsed">Collapsed</string>
108
105
@@ -121,9 +118,6 @@
121 <string name="default_agent">Default</string>
118 <string name="default_agent">Default</string>
122 <string name="default_agent_desc">Server default agent</string>
119 <string name="default_agent_desc">Server default agent</string>
123 <string name="no_commands">No commands reported by the server.</string>
120 <string name="no_commands">No commands reported by the server.</string>
124− <string name="loading_models">Loading models…</string>
125− <string name="loading_agents">Loading agents…</string>
126− <string name="loading_commands">Loading commands…</string>
127 <string name="saving">Saving…</string>
121 <string name="saving">Saving…</string>
128 <string name="image_failed">Failed to load image</string>
122 <string name="image_failed">Failed to load image</string>
129
123
@@ -163,7 +157,6 @@
163 <string name="no_crash_reports">No crashes recorded. \ud83d\udc4d</string>
157 <string name="no_crash_reports">No crashes recorded. \ud83d\udc4d</string>
164 <string name="clear_all">Clear all</string>
158 <string name="clear_all">Clear all</string>
165 <string name="crash_report">Crash report</string>
159 <string name="crash_report">Crash report</string>
166− <string name="diagnostics_share_subject">opencode-android crash report</string>
167
160
168 <!-- Notifications -->
161 <!-- Notifications -->
169 <string name="notif_channel_status">Agent activity</string>
162 <string name="notif_channel_status">Agent activity</string>