Make explicit service restart daemon-authoritative (#6072)

* Make service restarts daemon-authoritative

* Update CoreServiceManager.kt

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
This commit is contained in:
eliotcougarand2dust authored and GitHub committed 2026-08-16 15:26:58 +08:00
1 parent c1374a73fb
commit 780d194213
4 files changed
+86 -28

No files matched your search

@@ -1,5 +1,6 @@
package com.v2ray.ang.core
import android.app.Activity
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
@@ -30,6 +31,7 @@ import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import libv2ray.CoreCallbackHandler
import libv2ray.CoreController
@@ -474,9 +476,20 @@ object CoreServiceManager {
AppConfig.MSG_STATE_RESTART -> {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Restart service")
serviceControl.stopService()
Thread.sleep(500L)
LauncherManager.startService(serviceControl.getService())
// The UI and daemon run in separate processes, so acknowledge the active
// daemon before stopping it instead of relying on possibly stale UI state.
if (isOrderedBroadcast) resultCode = Activity.RESULT_OK
val pendingResult = goAsync()
CoroutineScope(Dispatchers.Default).launch {
try {
serviceControl.stopService()
delay(500L)
LauncherManager.startService(serviceControl.getService())
} finally {
pendingResult.finish()
}
}
}
AppConfig.MSG_MEASURE_DELAY -> {
@@ -56,6 +56,18 @@ object LauncherManager {
MessageHelper.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
}
/** Restarts the active daemon without starting a stopped service. */
fun restartService(context: Context) {
MessageHelper.sendMsg2Service(context, AppConfig.MSG_STATE_RESTART, "")
}
/** Restarts the active daemon, or delegates to the caller's permission-aware start flow. */
fun restartServiceOrStart(context: Context, startIfStopped: () -> Unit) {
MessageHelper.sendMsg2ServiceForResult(context, AppConfig.MSG_STATE_RESTART, "") { handled ->
if (!handled) startIfStopped()
}
}
@Throws(Exception::class)
private fun startContextService(context: Context) {
// Note: isRunning check is removed here to avoid loading Native libraries in the UI process.
@@ -1,5 +1,7 @@
package com.v2ray.ang.helper
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -27,6 +29,37 @@ object MessageHelper {
sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content)
}
/**
* Sends an ordered service message and reports whether a daemon receiver handled it.
* With no running daemon, the initial canceled result reaches [onResult] unchanged.
*/
internal fun sendMsg2ServiceForResult(
ctx: Context,
what: Int,
content: Serializable,
onResult: (handled: Boolean) -> Unit,
) {
val resultReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
onResult(resultCode == Activity.RESULT_OK)
}
}
try {
ctx.sendOrderedBroadcast(
messageIntent(AppConfig.BROADCAST_ACTION_SERVICE, what, content),
null,
resultReceiver,
null,
Activity.RESULT_CANCELED,
null,
null,
)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to send ordered message to service", e)
onResult(false)
}
}
/**
* Sends a message to the UI.
*
@@ -115,14 +148,16 @@ object MessageHelper {
*/
private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
try {
val intent = Intent()
intent.action = action
intent.`package` = AppConfig.ANG_PACKAGE
intent.putExtra("key", what)
intent.putExtra("content", content)
ctx.sendBroadcast(intent)
ctx.sendBroadcast(messageIntent(action, what, content))
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to send message with action: $action", e)
}
}
}
private fun messageIntent(action: String, what: Int, content: Serializable): Intent =
Intent(action).apply {
`package` = AppConfig.ANG_PACKAGE
putExtra("key", what)
putExtra("content", content)
}
}
@@ -75,10 +75,10 @@ class MainActivity : HelperBaseComponentActivity() {
val restartService = data.getBooleanExtra(
ProfileEditorResult.EXTRA_RESTART_SERVICE, false
)
val selectedProfileSaved = action == ProfileEditorResult.ACTION_SAVED &&
data.getStringExtra(ProfileEditorResult.EXTRA_GUID) == mainViewModel.uiState.value.selectedGuid
mainViewModel.onAction(MainAction.RefreshGroups)
if (restartService && mainViewModel.uiState.value.isRunning) {
restartV2Ray()
}
if (restartService || selectedProfileSaved) LauncherManager.restartService(this)
}
private val settingsActivityLauncher =
@@ -87,7 +87,7 @@ class MainActivity : HelperBaseComponentActivity() {
val refreshGroups = SettingsChangeManager.consumeSetupGroupTab()
mainViewModel.refreshUiSettings()
if (refreshGroups) mainViewModel.onAction(MainAction.RefreshGroups)
if (restartService && mainViewModel.uiState.value.isRunning) restartV2Ray()
if (restartService) LauncherManager.restartService(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -110,7 +110,7 @@ class MainActivity : HelperBaseComponentActivity() {
MainAction.ImportClipboard -> importClipboard()
MainAction.ImportConfigLocal -> importConfigLocal()
is MainAction.ImportManually -> importManually(action.type)
MainAction.RestartService -> restartV2Ray()
MainAction.RestartService -> LauncherManager.restartServiceOrStart(this, ::requestServiceStart)
MainAction.LocateSelectedServer -> mainViewModel.triggerLocateSelectedServer()
is MainAction.SelectServer -> setSelectServer(action.guid)
is MainAction.EditServer -> editServer(action.guid, action.profile)
@@ -161,14 +161,20 @@ class MainActivity : HelperBaseComponentActivity() {
private fun handleFabAction() {
if (mainViewModel.uiState.value.isRunning) {
LauncherManager.stopService(this)
} else if (SettingsManager.isVpnMode()) {
val intent = VpnService.prepare(this)
if (intent == null) startV2Ray() else requestVpnPermission.launch(intent)
} else {
startV2Ray()
requestServiceStart()
}
}
private fun requestServiceStart() {
if (!SettingsManager.isVpnMode()) {
startV2Ray()
return
}
val intent = VpnService.prepare(this)
if (intent == null) startV2Ray() else requestVpnPermission.launch(intent)
}
private fun handleLayoutTestClick() {
if (mainViewModel.uiState.value.isRunning) {
mainViewModel.testCurrentServerRealPing()
@@ -188,14 +194,6 @@ class MainActivity : HelperBaseComponentActivity() {
LauncherManager.startService(this)
}
private fun restartV2Ray() {
if (mainViewModel.uiState.value.isRunning) LauncherManager.stopService(this)
lifecycleScope.launch {
kotlinx.coroutines.delay(500)
startV2Ray()
}
}
private fun importManually(createConfigType: Int) {
val intent = when (createConfigType) {
EConfigType.POLICYGROUP.value -> Intent(this, ServerGroupActivity::class.java)
@@ -275,7 +273,7 @@ class MainActivity : HelperBaseComponentActivity() {
val selected = mainViewModel.uiState.value.selectedGuid
if (guid != selected) {
mainViewModel.updateSelectedGuid(guid)
if (mainViewModel.uiState.value.isRunning) restartV2Ray()
LauncherManager.restartService(this)
}
}