@@ -9,8 +9,8 @@ import kotlinx.coroutines.flow.asStateFlow
99import kotlinx.coroutines.flow.update
1010import android.util.Log
1111import kotlinx.coroutines.launch
12−import kotlinx.coroutines.runBlocking
1312import kotlinx.coroutines.withContext
13+import java.util.concurrent.Executors
1414
1515/**
1616 * Persists the in-progress message draft for each session so it survives
@@ -36,6 +36,12 @@ class DraftStore(context: Context, scope: CoroutineScope) {
3636 * background thread to avoid blocking on first-time disk I/O. */
3737 private val prefsInitialized = java.util.concurrent.atomic.AtomicBoolean(false)
3838
39+ /** Single-thread executor for flushDraft when prefs aren't initialized yet,
40+ * so the main thread is never blocked on disk I/O. */
41+ private val flushExecutor = Executors.newSingleThreadExecutor { r ->
42+ Thread(r, "DraftStore-flush").apply { isDaemon = true }
43+ }
44+
3945 private val _drafts = MutableStateFlow<Map<String, String>>(emptyMap())
4046 /** The full in-memory snapshot of all drafts, updated when prefs finish loading. */
4147 val drafts: StateFlow<Map<String, String>> = _drafts.asStateFlow()
@@ -60,6 +66,17 @@ class DraftStore(context: Context, scope: CoroutineScope) {
6066 /** Returns the draft for [sessionId], or "" if not found or still loading. */
6167 fun get(sessionId: String): String = _drafts.value[sessionId].orEmpty()
6268
69+ /** Update the in-memory draft immediately without persisting to disk.
70+ * Used when navigation must see the new value synchronously (e.g. share-intent
71+ * injection before ChatScreen composes). Call [set] afterwards to persist. */
72+ fun setImmediate(sessionId: String, text: String) {
73+ _drafts.update { current ->
74+ val updated = current.toMutableMap()
75+ if (text.isBlank()) updated.remove(sessionId) else updated[sessionId] = text
76+ updated
77+ }
78+ }
79+
6380 suspend fun set(sessionId: String, text: String) {
6481 _drafts.update { current ->
6582 val updated = current.toMutableMap()
@@ -98,9 +115,10 @@ class DraftStore(context: Context, scope: CoroutineScope) {
98115 * [apply] (asynchronous) and returns immediately without blocking the main thread.
99116 *
100117 * If the prefs file has NOT been opened yet (a rare race where onCleared fires
101− * before the background init), accessing the lazy would synchronously load and
102− * parse the XML on the main thread. To avoid this, the write is deferred to a
103− * blocking call on the IO dispatcher instead.
118+ * before the background init), the write is dispatched to a background thread
119+ * instead of blocking the main thread on a synchronous disk read. The draft
120+ * may be lost if the process exits before the write completes, but this is
121+ * preferable to an ANR.
104122 */
105123 fun flushDraft(sessionId: String, text: String) {
106124 _drafts.update { current ->
@@ -116,16 +134,16 @@ class DraftStore(context: Context, scope: CoroutineScope) {
116134 }.apply()
117135 }.onFailure { Log.w("DraftStore", "Failed to flush draft for $sessionId", it) }
118136 } else {
119− // Prefs not yet opened — avoid a synchronous disk read on the main thread
120− // by blocking on IO instead. This is acceptable in onCleared() because the
121− // viewModelScope is already dead and there's no suspending alternative.
122− runCatching {
123− runBlocking(Dispatchers.IO) {
137+ // Prefs not yet opened — dispatch to a background thread to avoid a
138+ // synchronous disk read on the main thread (which would ANR). The draft
139+ // may be lost if the process exits before the write completes.
140+ flushExecutor.execute {
141+ runCatching {
124142 prefs.edit().apply {
125143 if (text.isBlank()) remove(sessionId) else putString(sessionId, text)
126144 }.apply()
127− }
128− }.onFailure { Log.w("DraftStore", "Failed to flush draft for $sessionId", it) }
145+ }.onFailure { Log.w("DraftStore", "Failed to flush draft for $sessionId", it) }
146+ }
129147 }
130148 }
131149}