diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c3dd21f47..0ba626f30 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -134,8 +134,15 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
index 9d1c34405..ede20bfb1 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
@@ -230,7 +230,11 @@ class BaseService {
profileId > 0L && SagerDatabase.proxyDao.getById(profileId) != null ->
DataStore.selectedProxy = profileId
}
- onMainDispatcher { reloadInner(reloadStopGeneration) }
+ // Compute the in-place selector decision here (off the main thread) so
+ // reloadInner() does no DAO reads on the UI thread (Plan 027). null tag =>
+ // no fast-path (fall through to the state machine).
+ val selectorTag = resolveSelectorReloadTag()
+ onMainDispatcher { reloadInner(reloadStopGeneration, selectorTag) }
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
@@ -245,7 +249,7 @@ class BaseService {
}
}
- private fun reloadInner(reloadStopGeneration: Long) {
+ private fun reloadInner(reloadStopGeneration: Long, selectorTag: String?) {
// A stop raced the async refresh: drop this stale reload so it can't restart a service
// the user stopped. (A Connecting->Connected transition does NOT bump stopGeneration,
// so an in-flight legitimate reload still applies.)
@@ -258,16 +262,13 @@ class BaseService {
// Only take the in-place selector fast-path when fully Connected: during Connecting
// data.proxy is set but proxy.init() may not have built config/box yet, and during
// Stopping the box is being torn down — touching them would throw or act on a dead
- // instance. In those states fall through to the state machine below.
- if (s == State.Connected && canReloadSelector()) {
- val ent = SagerDatabase.proxyDao.getById(DataStore.selectedProxy)
- val tag = data.proxy!!.config.profileTagMap[ent?.id] ?: ""
- if (tag.isNotBlank() && ent != null) {
- // select from GUI
- data.proxy!!.box.selectOutbound(tag)
- // or select from webui
- // => selector_OnProxySelected
- }
+ // instance. In those states fall through to the state machine below. selectorTag was
+ // resolved off the main thread by the caller (null => no fast-path).
+ if (s == State.Connected && selectorTag != null && selectorTag.isNotBlank()) {
+ // select from GUI
+ data.proxy!!.box.selectOutbound(selectorTag)
+ // or select from webui
+ // => selector_OnProxySelected
return
}
when {
@@ -277,6 +278,18 @@ class BaseService {
}
}
+ // Off-main-thread resolver for reloadInner's in-place selector fast-path. Returns the
+ // outbound tag to select, or null if the selector fast-path does not apply. Does all the
+ // DAO reads (proxy/group) so reloadInner touches no DB on the UI thread.
+ fun resolveSelectorReloadTag(): String? {
+ if (data.state != State.Connected) return null
+ if (!canReloadSelector()) return null
+ val ent = SagerDatabase.proxyDao.getById(DataStore.selectedProxy) ?: return null
+ val proxy = data.proxy ?: return null
+ val tag = proxy.config.profileTagMap[ent.id] ?: ""
+ return tag.ifBlank { null }
+ }
+
fun canReloadSelector(): Boolean {
val running = data.proxy?.lastSelectorGroupId ?: -1L
if (running < 0L) return false
@@ -535,11 +548,16 @@ class BaseService {
return runOnMainDispatcher {
try {
- data.notification = createNotification(ServiceNotification.genTitle(profile))
+ // Reuse the title computed during ProxyInstance construction (off the main
+ // thread); calling genTitle() here would do a groupDao read on the main thread.
+ data.notification = createNotification(proxy.displayProfileName)
Executable.killAll() // clean up old processes
preInit()
- proxy.init()
+ // buildConfig() (via proxy.init()) does synchronous group/profile DAO reads;
+ // run it off the main thread so it works with the main-thread-DB allowance
+ // removed (Plan 027). init() is suspend and does not touch the UI.
+ onDefaultDispatcher { proxy.init() }
DataStore.currentProfile = profile.id
proxy.processes = GuardedProcessPool {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt
index 0f4e9d290..55e579356 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt
@@ -6,6 +6,7 @@ import io.nekohasekai.sagernet.bg.ServiceNotification
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.utils.Commandline
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.runBlocking
import moe.matsuri.nb4a.utils.JavaUtil
@@ -70,7 +71,10 @@ class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? =
override fun close() {
super.close()
- runBlocking {
+ // Teardown is called on the main thread; the final traffic flush in looper.stop() does
+ // synchronous DAO writes, so run the blocking body on a background dispatcher to keep it
+ // off the UI thread (Plan 027 — main-thread-DB allowance removed).
+ runBlocking(Dispatchers.Default) {
looper?.stop()
looper = null
}