From b1e41338cadb7a07d6c3feff79d6d3c2cf5380dc Mon Sep 17 00:00:00 2001 From: Craun718 <3653544699@qq.com> Date: Sun, 30 Aug 2026 17:23:07 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=9B=AE=E6=A0=87=E5=BA=94=E7=94=A8=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E9=87=8D=E5=90=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aliothmoon/maafw/di/RunnerModule.kt | 3 +- .../com/aliothmoon/maafw/remote/MaaRunner.kt | 6 +- .../maafw/remote/internal/ActivityUtils.kt | 29 +++++++- .../maafw/runner/MaaFrameworkRunnerPort.kt | 3 + .../aliothmoon/maafw/runner/RunPlanPayload.kt | 5 ++ .../maafw/session/SessionContracts.kt | 5 ++ .../maafw/session/SessionViewModel.kt | 7 ++ .../aliothmoon/maafw/settings/AppSettings.kt | 4 + .../maafw/settings/AppSettingsGateway.kt | 4 + .../maafw/settings/AppSettingsManager.kt | 8 ++ .../maafw/ui/settings/SettingsScreen.kt | 11 +++ app/src/main/res/values-en/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../maafw/privileged/FakePrivilegedService.kt | 3 + .../maafw/remote/ActivityUtilsDecisionTest.kt | 73 +++++++++++++++++++ .../runner/MaaFrameworkRunnerPortTest.kt | 16 ++++ .../maafw/session/SessionViewModelTest.kt | 16 ++++ .../maafw/settings/FakeAppSettingsGateway.kt | 6 ++ 18 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 app/src/test/java/com/aliothmoon/maafw/remote/ActivityUtilsDecisionTest.kt diff --git a/app/src/main/java/com/aliothmoon/maafw/di/RunnerModule.kt b/app/src/main/java/com/aliothmoon/maafw/di/RunnerModule.kt index 7d058952..51b5ca54 100644 --- a/app/src/main/java/com/aliothmoon/maafw/di/RunnerModule.kt +++ b/app/src/main/java/com/aliothmoon/maafw/di/RunnerModule.kt @@ -44,6 +44,7 @@ val runnerModule = module { nativeLibraryDir = context.applicationInfo.nativeLibraryDir, runMode = get().runMode::value, resolutionPreference = get().resolutionPreference::value, + forceRestartApp = get().forceRestartApp::value, debugMode = get().debugMode::value, scope = get(named()), servicePort = get(), @@ -137,4 +138,4 @@ val runnerModule = module { journal = get(), ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aliothmoon/maafw/remote/MaaRunner.kt b/app/src/main/java/com/aliothmoon/maafw/remote/MaaRunner.kt index 5ce61c1e..3d8c5f3b 100644 --- a/app/src/main/java/com/aliothmoon/maafw/remote/MaaRunner.kt +++ b/app/src/main/java/com/aliothmoon/maafw/remote/MaaRunner.kt @@ -12,6 +12,7 @@ import com.aliothmoon.maafw.maa.MaaFwVersion import com.aliothmoon.maafw.maa.MaaGlobalOption import com.aliothmoon.maafw.maa.MaaLoggingLevel import com.aliothmoon.maafw.maa.MaaStatus +import com.aliothmoon.maafw.remote.internal.ActivityUtils import com.aliothmoon.maafw.remote.internal.PrimaryDisplayManager import com.aliothmoon.maafw.remote.internal.VirtualDisplayManager import com.aliothmoon.maafw.runner.AgentPayload @@ -292,6 +293,7 @@ class MaaRunner(private val agentHost: AgentHost) { // TouchArgs.contact 是 v5.12.3 起 fw 才填的合约字段,旧 fw 那 4 字节是栈残值, // 不过闸直接读会得到幻影手指;低于配对版本一律压 0(等价旧 bridge 单指行为) NativeBridgeLib.setContactSupport(MaaFwVersion.fillsTouchContact(lib.MaaVersion())) + ActivityUtils.forceRestartOnVirtualDisplay = payload.forceRestartApp val displayId = when (payload.displayMode) { DisplayMode.PRIMARY -> @@ -484,8 +486,8 @@ class MaaRunner(private val agentHost: AgentHost) { * screen_resolution 必须与帧缓冲、触摸坐标空间三者一致,不一致时 screencap 立即失败 * library_path 用裸名:bridge 已在本进程加载,控制单元 dlopen 同名即命中同一份 * - * 虚拟屏模式下 force_stop 必须为 true:目标应用若已在主屏上跑着,startActivity 会复用它在主屏的 - * 既有 task,虚拟屏上拿不到画面。先杀掉再拉起,进程才会落到虚拟屏上 + * 虚拟屏模式下 controller 继续请求 force_stop:目标应用若已在主屏上跑着,startActivity 会复用 + * 它在主屏的既有 task,虚拟屏上拿不到画面。是否真的强停由 ActivityUtils 按用户设置判定 * 主屏模式反过来——目标就在主屏,杀掉只会把用户已经摆好的现场清空 */ private fun buildControllerConfig(payload: RunPlanPayload, displayId: Int): String { diff --git a/app/src/main/java/com/aliothmoon/maafw/remote/internal/ActivityUtils.kt b/app/src/main/java/com/aliothmoon/maafw/remote/internal/ActivityUtils.kt index 618f3432..b34065c5 100644 --- a/app/src/main/java/com/aliothmoon/maafw/remote/internal/ActivityUtils.kt +++ b/app/src/main/java/com/aliothmoon/maafw/remote/internal/ActivityUtils.kt @@ -32,6 +32,10 @@ object ActivityUtils { @Volatile var forceFullscreenOnVirtualDisplay: Boolean = false + /** false 时仅在目标应用未确认位于目标虚拟屏的情况下强停;进程默认 true 以保持旧行为 */ + @Volatile + var forceRestartOnVirtualDisplay: Boolean = true + private val setLaunchWindowingMode by lazy { runCatching { ActivityOptions::class.java @@ -85,6 +89,18 @@ object ActivityUtils { @JvmStatic fun packageNameOf(spec: String): String = componentOf(spec)?.packageName ?: spec + /** 无法确认 display 时返回 true,保守沿用旧强停行为 */ + internal fun shouldForceStopBeforeStart( + requestedForceStop: Boolean, + targetDisplayId: Int, + currentDisplayId: Int?, + forceRestart: Boolean, + ): Boolean = requestedForceStop && ( + targetDisplayId == Display.DEFAULT_DISPLAY || + forceRestart || + currentDisplayId != targetDisplayId + ) + private fun componentOf(spec: String): ComponentName? = spec.takeIf { it.contains('/') }?.let { ComponentName.unflattenFromString(it) } @@ -121,8 +137,19 @@ object ActivityUtils { } intent.addFlags(flag) - if (forceStop) { + val currentDisplayId = if ( + forceStop && + !forceRestartOnVirtualDisplay && + displayId != Display.DEFAULT_DISPLAY + ) { + getAppDisplayId(targetPackage) + } else { + null + } + if (shouldForceStopBeforeStart(forceStop, displayId, currentDisplayId, forceRestartOnVirtualDisplay)) { ServiceManager.getActivityManager().forceStopPackage(targetPackage) + } else { + Ln.i("startApp keeps $targetPackage alive on display $displayId") } Ln.i("startApp ${intent.component?.flattenToShortString()}") diff --git a/app/src/main/java/com/aliothmoon/maafw/runner/MaaFrameworkRunnerPort.kt b/app/src/main/java/com/aliothmoon/maafw/runner/MaaFrameworkRunnerPort.kt index 85ab875c..b1a2992f 100644 --- a/app/src/main/java/com/aliothmoon/maafw/runner/MaaFrameworkRunnerPort.kt +++ b/app/src/main/java/com/aliothmoon/maafw/runner/MaaFrameworkRunnerPort.kt @@ -51,6 +51,8 @@ class MaaFrameworkRunnerPort( private val runMode: () -> RunMode, /** 同上:分辨率偏好可能两轮之间被改 */ private val resolutionPreference: () -> ResolutionPreference, + /** 同上:StartApp 的强停策略可能两轮之间被改 */ + private val forceRestartApp: () -> Boolean, /** 调试模式:传给特权进程 setup 的 isDebug,开启 MaaFramework 详细日志 */ private val debugMode: () -> Boolean, private val scope: CoroutineScope, @@ -324,6 +326,7 @@ class MaaFrameworkRunnerPort( screenWidth = width, screenHeight = height, displayMode = mode.displayMode, + forceRestartApp = forceRestartApp(), tasks = plan.tasks.map { RuntimeTaskPayload( taskName = it.taskName, diff --git a/app/src/main/java/com/aliothmoon/maafw/runner/RunPlanPayload.kt b/app/src/main/java/com/aliothmoon/maafw/runner/RunPlanPayload.kt index 36ba1b7b..d942b2aa 100644 --- a/app/src/main/java/com/aliothmoon/maafw/runner/RunPlanPayload.kt +++ b/app/src/main/java/com/aliothmoon/maafw/runner/RunPlanPayload.kt @@ -21,6 +21,11 @@ data class RunPlanPayload( val screenHeight: Int, /** [com.aliothmoon.maafw.constant.DisplayMode] 取值;决定 Runner 找谁要屏幕尺寸 */ val displayMode: Int = DisplayMode.BACKGROUND, + /** + * 后台模式 StartApp 前是否无条件强停目标应用。 + * 线格式默认 true 是为了旧 payload 解码后保持既有行为;用户设置本身默认 false。 + */ + val forceRestartApp: Boolean = true, val tasks: List, /** PI 声明的 agent,按声明顺序;空表示本次不起 agent */ val agents: List = emptyList(), diff --git a/app/src/main/java/com/aliothmoon/maafw/session/SessionContracts.kt b/app/src/main/java/com/aliothmoon/maafw/session/SessionContracts.kt index c6753ace..d910011d 100644 --- a/app/src/main/java/com/aliothmoon/maafw/session/SessionContracts.kt +++ b/app/src/main/java/com/aliothmoon/maafw/session/SessionContracts.kt @@ -63,6 +63,8 @@ data class SessionUiState( /** 全局的跑完关目标应用;与 ScheduleStrategy 上的同名选项并存,本项优先 */ val closeAppAfterTask: Boolean = false, val touchPreviewEnabled: Boolean = true, + /** 后台模式 StartApp 前是否无条件强停目标应用 */ + val forceRestartApp: Boolean = false, /** 定时任务的亮屏解锁;逐条规则的收尾选项在 ScheduleStrategy 上,不在这 */ val wakeUnlockEnabled: Boolean = false, val wakeCredential: String = "", @@ -254,6 +256,9 @@ sealed interface SessionIntent { /** 预览上是否画注入的触点 */ data class SetTouchPreviewEnabled(val enabled: Boolean) : SessionIntent + /** 后台模式 StartApp 前是否无条件强停目标应用;改动下一轮生效 */ + data class SetForceRestartApp(val enabled: Boolean) : SessionIntent + data class SetTelemetryEnabled(val enabled: Boolean) : SessionIntent /** 立刻关掉目标应用:停虚拟屏,屏上的应用跟着一起没 */ diff --git a/app/src/main/java/com/aliothmoon/maafw/session/SessionViewModel.kt b/app/src/main/java/com/aliothmoon/maafw/session/SessionViewModel.kt index 5ac61ff2..770a0aed 100644 --- a/app/src/main/java/com/aliothmoon/maafw/session/SessionViewModel.kt +++ b/app/src/main/java/com/aliothmoon/maafw/session/SessionViewModel.kt @@ -72,6 +72,7 @@ private data class SettingsSnapshot( val resolutionPreference: ResolutionPreference, val debugMode: Boolean, val themeStyle: ThemeStyle = ThemeStyle.DEFAULT, + val forceRestartApp: Boolean = false, val env: EnvSnapshot = EnvSnapshot(), val quick: QuickSnapshot = QuickSnapshot(), ) @@ -137,6 +138,8 @@ class SessionViewModel( SettingsSnapshot(runMode, overlayMode, screenSaver, resolution, debug) }.combine(appSettings.themeStyle) { snapshot, style -> snapshot.copy(themeStyle = style) + }.combine(appSettings.forceRestartApp) { snapshot, forceRestart -> + snapshot.copy(forceRestartApp = forceRestart) }.combine( combine(appSettings.wakeUnlockEnabled, appSettings.wakeCredential, ::EnvSnapshot), ) { snapshot, env -> snapshot.copy(env = env) } @@ -260,6 +263,7 @@ class SessionViewModel( screenSaverEnabled = settings.screenSaverEnabled, closeAppAfterTask = settings.quick.closeAppAfterTask, touchPreviewEnabled = settings.quick.touchPreviewEnabled, + forceRestartApp = settings.forceRestartApp, telemetryEnabled = settings.quick.telemetryEnabled, wakeUnlockEnabled = settings.env.wakeUnlockEnabled, wakeCredential = settings.env.wakeCredential, @@ -475,6 +479,9 @@ class SessionViewModel( is SessionIntent.SetTouchPreviewEnabled -> appSettings.setTouchPreviewEnabled(intent.enabled) + is SessionIntent.SetForceRestartApp -> + appSettings.setForceRestartApp(intent.enabled) + SessionIntent.ShowOverlay -> openControlOverlay() SessionIntent.ApplyForegroundResolution -> applyForegroundResolution() SessionIntent.ResetForegroundResolution -> resetForegroundResolution() diff --git a/app/src/main/java/com/aliothmoon/maafw/settings/AppSettings.kt b/app/src/main/java/com/aliothmoon/maafw/settings/AppSettings.kt index 17440f63..843b09d5 100644 --- a/app/src/main/java/com/aliothmoon/maafw/settings/AppSettings.kt +++ b/app/src/main/java/com/aliothmoon/maafw/settings/AppSettings.kt @@ -58,6 +58,10 @@ data class AppSettings( @PrefKey(default = "true") val touchPreviewEnabled: String = "true", + /** 后台模式下 StartApp 前是否无条件强停目标应用;默认关,已在当前虚拟屏时保留现场 */ + @PrefKey(default = "false") + val forceRestartApp: String = "false", + /** [com.aliothmoon.maafw.runner.ResolutionPreference] 的 name */ @PrefKey(default = "P720") val resolutionPreference: String = "P720", diff --git a/app/src/main/java/com/aliothmoon/maafw/settings/AppSettingsGateway.kt b/app/src/main/java/com/aliothmoon/maafw/settings/AppSettingsGateway.kt index 2461eb83..d5863ef6 100644 --- a/app/src/main/java/com/aliothmoon/maafw/settings/AppSettingsGateway.kt +++ b/app/src/main/java/com/aliothmoon/maafw/settings/AppSettingsGateway.kt @@ -30,6 +30,10 @@ interface AppSettingsGateway { val touchPreviewEnabled: StateFlow suspend fun setTouchPreviewEnabled(enabled: Boolean) + /** 后台模式 StartApp 前是否无条件强停目标应用 */ + val forceRestartApp: StateFlow + suspend fun setForceRestartApp(enabled: Boolean) + val resolutionPreference: StateFlow suspend fun setResolutionPreference(preference: ResolutionPreference) diff --git a/app/src/main/java/com/aliothmoon/maafw/settings/AppSettingsManager.kt b/app/src/main/java/com/aliothmoon/maafw/settings/AppSettingsManager.kt index bfedb568..39f42289 100644 --- a/app/src/main/java/com/aliothmoon/maafw/settings/AppSettingsManager.kt +++ b/app/src/main/java/com/aliothmoon/maafw/settings/AppSettingsManager.kt @@ -81,6 +81,9 @@ class AppSettingsManager(private val context: Context) : AppSettingsGateway { private val _touchPreviewEnabled = MutableStateFlow(defaults.touchPreviewEnabled.toBoolean()) override val touchPreviewEnabled: StateFlow = _touchPreviewEnabled.asStateFlow() + private val _forceRestartApp = MutableStateFlow(defaults.forceRestartApp.toBoolean()) + override val forceRestartApp: StateFlow = _forceRestartApp.asStateFlow() + private val _eventNotificationLevel = MutableStateFlow(parseEventNotificationLevel(defaults.eventNotificationLevel)) val eventNotificationLevel: StateFlow = _eventNotificationLevel.asStateFlow() @@ -130,6 +133,7 @@ class AppSettingsManager(private val context: Context) : AppSettingsGateway { _screenSaverEnabled.value = s.screenSaverEnabled.toBoolean() _closeAppAfterTask.value = s.closeAppAfterTask.toBoolean() _touchPreviewEnabled.value = s.touchPreviewEnabled.toBoolean() + _forceRestartApp.value = s.forceRestartApp.toBoolean() _resolutionPreference.value = parseResolutionPreference(s.resolutionPreference) _debugMode.value = s.debugMode.toBoolean() _themeStyle.value = parseThemeStyle(s.themeStyle) @@ -183,6 +187,10 @@ class AppSettingsManager(private val context: Context) : AppSettingsGateway { context.dataStore.edit { it[touchPreviewEnabled] = enabled.toString() } } + override suspend fun setForceRestartApp(enabled: Boolean): Unit = with(AppSettingsSchema) { + context.dataStore.edit { it[forceRestartApp] = enabled.toString() } + } + override suspend fun setResolutionPreference(preference: ResolutionPreference): Unit = with(AppSettingsSchema) { context.dataStore.edit { it[resolutionPreference] = preference.name } } diff --git a/app/src/main/java/com/aliothmoon/maafw/ui/settings/SettingsScreen.kt b/app/src/main/java/com/aliothmoon/maafw/ui/settings/SettingsScreen.kt index 924e51ed..33f4ecde 100644 --- a/app/src/main/java/com/aliothmoon/maafw/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/aliothmoon/maafw/ui/settings/SettingsScreen.kt @@ -614,6 +614,17 @@ private fun OtherCard( enabled = !locked, onSelect = { onIntent(SessionIntent.SetResolutionPreference(it)) }, ) + Spacer(Modifier.height(MaaDesignTokens.Spacing.sm)) + MaaSwitchRow( + label = stringResource(R.string.settings_force_restart_app), + checked = state.forceRestartApp, + onCheckedChange = { onIntent(SessionIntent.SetForceRestartApp(it)) }, + ) + Text( + text = stringResource(R.string.settings_force_restart_app_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) // 开发版 PI 一律不上报(TelemetryController 也照此拦),留个点不动的开关只会让人以为坏了 if (state.telemetryDeclared && !state.telemetryLockedByVersion) { Spacer(Modifier.height(MaaDesignTokens.Spacing.sm)) diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 4604fd68..d1227709 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -131,6 +131,8 @@ Foreground mode Runs on a separate virtual display so the phone stays usable; resolution derived from the project declaration Drives the current screen directly, occupying the phone while running; the target app is not restarted and preview touch is disabled + Force-restart the target app + Try enabling this when the target app cannot start normally Server selection The project provides no selectable servers Current resource diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 49c3628e..8ee671a5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -125,6 +125,8 @@ 前台模式 在独立的虚拟屏幕上运行任务,手机可正常使用;分辨率由项目声明推导 直接操作当前屏幕,运行期间手机被占用;不会重启目标应用,预览里也不接管触摸 + 强制重启目标应用 + 无法正常启动目标app时可尝试开启此项 服务器选择 项目未提供可选服务器 当前资源 diff --git a/app/src/test/java/com/aliothmoon/maafw/privileged/FakePrivilegedService.kt b/app/src/test/java/com/aliothmoon/maafw/privileged/FakePrivilegedService.kt index 6c8b9ddc..598c3b18 100644 --- a/app/src/test/java/com/aliothmoon/maafw/privileged/FakePrivilegedService.kt +++ b/app/src/test/java/com/aliothmoon/maafw/privileged/FakePrivilegedService.kt @@ -37,6 +37,8 @@ open class FakePrivilegedService : RemoteService { var running: Boolean = false var setupResult: Boolean = true var startRunResult: Boolean = true + var startRunJsons: MutableList = mutableListOf() + private set var stopRunCount: Int = 0 private set @@ -91,6 +93,7 @@ open class FakePrivilegedService : RemoteService { } override fun startRun(runPlanJson: String?): Boolean { if (!startRunResult) return false + startRunJsons += runPlanJson.orEmpty() running = true return true } diff --git a/app/src/test/java/com/aliothmoon/maafw/remote/ActivityUtilsDecisionTest.kt b/app/src/test/java/com/aliothmoon/maafw/remote/ActivityUtilsDecisionTest.kt new file mode 100644 index 00000000..e031ffc7 --- /dev/null +++ b/app/src/test/java/com/aliothmoon/maafw/remote/ActivityUtilsDecisionTest.kt @@ -0,0 +1,73 @@ +package com.aliothmoon.maafw.remote.internal + +import android.view.Display +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class ActivityUtilsDecisionTest { + + @Test + fun `force restart is skipped only when app is confirmed on target virtual display`() { + val targetDisplay = 12 + + assertFalse( + ActivityUtils.shouldForceStopBeforeStart( + requestedForceStop = true, + targetDisplayId = targetDisplay, + currentDisplayId = targetDisplay, + forceRestart = false, + ), + ) + } + + @Test + fun `main display or unknown placement keeps legacy force restart`() { + val targetDisplay = 12 + + assertTrue( + ActivityUtils.shouldForceStopBeforeStart( + requestedForceStop = true, + targetDisplayId = targetDisplay, + currentDisplayId = Display.DEFAULT_DISPLAY, + forceRestart = false, + ), + ) + assertTrue( + ActivityUtils.shouldForceStopBeforeStart( + requestedForceStop = true, + targetDisplayId = targetDisplay, + currentDisplayId = null, + forceRestart = false, + ), + ) + } + + @Test + fun `explicit force restart and primary display preserve current behavior`() { + assertTrue( + ActivityUtils.shouldForceStopBeforeStart( + requestedForceStop = true, + targetDisplayId = Display.DEFAULT_DISPLAY, + currentDisplayId = Display.DEFAULT_DISPLAY, + forceRestart = false, + ), + ) + assertTrue( + ActivityUtils.shouldForceStopBeforeStart( + requestedForceStop = true, + targetDisplayId = 12, + currentDisplayId = 12, + forceRestart = true, + ), + ) + assertFalse( + ActivityUtils.shouldForceStopBeforeStart( + requestedForceStop = false, + targetDisplayId = 12, + currentDisplayId = Display.DEFAULT_DISPLAY, + forceRestart = true, + ), + ) + } +} diff --git a/app/src/test/java/com/aliothmoon/maafw/runner/MaaFrameworkRunnerPortTest.kt b/app/src/test/java/com/aliothmoon/maafw/runner/MaaFrameworkRunnerPortTest.kt index 2106250d..5aa875dd 100644 --- a/app/src/test/java/com/aliothmoon/maafw/runner/MaaFrameworkRunnerPortTest.kt +++ b/app/src/test/java/com/aliothmoon/maafw/runner/MaaFrameworkRunnerPortTest.kt @@ -20,8 +20,10 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import kotlinx.serialization.decodeFromString import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -67,6 +69,7 @@ class MaaFrameworkRunnerPortTest { scope: TestScope, service: FakePrivilegedService = FakePrivilegedService(), servicePort: FakePrivilegedServicePort = FakePrivilegedServicePort(service), + forceRestartApp: () -> Boolean = { false }, ): Pair { val installer = mockk() every { installer.installedDir() } returns temp.newFolder("pi") @@ -76,6 +79,7 @@ class MaaFrameworkRunnerPortTest { nativeLibraryDir = "/lib", runMode = { RunMode.BACKGROUND }, resolutionPreference = { ResolutionPreference.P720 }, + forceRestartApp = forceRestartApp, debugMode = { false }, scope = scope.backgroundScope, servicePort = servicePort, @@ -85,6 +89,18 @@ class MaaFrameworkRunnerPortTest { return runner to servicePort } + @Test + fun `force restart preference is carried in run payload`() = runTest(dispatcher) { + val service = FakePrivilegedService() + val (runner, _) = port(this, service) + + runner.start(plan()) + advanceUntilIdle() + + val payload = runPlanWireJson.decodeFromString(service.startRunJsons.single()) + assertFalse(payload.forceRestartApp) + } + @Test fun `stop during prepare is retried after start returns`() = runTest(dispatcher) { val service = FakePrivilegedService() diff --git a/app/src/test/java/com/aliothmoon/maafw/session/SessionViewModelTest.kt b/app/src/test/java/com/aliothmoon/maafw/session/SessionViewModelTest.kt index 73b6fd8b..1abf245b 100644 --- a/app/src/test/java/com/aliothmoon/maafw/session/SessionViewModelTest.kt +++ b/app/src/test/java/com/aliothmoon/maafw/session/SessionViewModelTest.kt @@ -566,6 +566,22 @@ class SessionViewModelTest { assertEquals(ResolutionPreference.P1080.resolution, vm.uiState.value.previewResolution) } + @Test + fun `force restart setting is persisted and projected into ui state`() = runTest(mainDispatcher) { + val settings = FakeAppSettingsGateway() + val (vm, _, _) = createVm(settings = settings) + backgroundScope.launch { vm.uiState.collect {} } + advanceUntilIdle() + + assertFalse(vm.uiState.value.forceRestartApp) + + vm.onIntent(SessionIntent.SetForceRestartApp(true)) + advanceUntilIdle() + + assertTrue(settings.forceRestartApp.value) + assertTrue(vm.uiState.value.forceRestartApp) + } + @Test fun `theme change is allowed while busy`() = runTest(mainDispatcher) { val runner = StubRunnerPort( diff --git a/app/src/test/java/com/aliothmoon/maafw/settings/FakeAppSettingsGateway.kt b/app/src/test/java/com/aliothmoon/maafw/settings/FakeAppSettingsGateway.kt index b846e55e..d2e25ba9 100644 --- a/app/src/test/java/com/aliothmoon/maafw/settings/FakeAppSettingsGateway.kt +++ b/app/src/test/java/com/aliothmoon/maafw/settings/FakeAppSettingsGateway.kt @@ -40,6 +40,12 @@ class FakeAppSettingsGateway : AppSettingsGateway { touchPreviewEnabled.value = enabled } + override val forceRestartApp = MutableStateFlow(false) + + override suspend fun setForceRestartApp(enabled: Boolean) { + forceRestartApp.value = enabled + } + override val resolutionPreference = MutableStateFlow(ResolutionPreference.P720) override suspend fun setResolutionPreference(preference: ResolutionPreference) { From 077ed78158b49ebb9bbb8e44fea30d932bf2b0f8 Mon Sep 17 00:00:00 2001 From: Craun718 <3653544699@qq.com> Date: Mon, 31 Aug 2026 00:23:52 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=BC=BA=E5=88=B6=E9=87=8D?= =?UTF-8?q?=E5=90=AF=E7=9B=AE=E6=A0=87=E5=BA=94=E7=94=A8=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=BC=80=E5=90=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/aliothmoon/maafw/runner/RunPlanPayload.kt | 2 +- .../java/com/aliothmoon/maafw/session/SessionContracts.kt | 2 +- .../java/com/aliothmoon/maafw/session/SessionViewModel.kt | 2 +- .../java/com/aliothmoon/maafw/settings/AppSettings.kt | 6 +++--- .../com/aliothmoon/maafw/session/SessionViewModelTest.kt | 8 ++++---- .../aliothmoon/maafw/settings/FakeAppSettingsGateway.kt | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/aliothmoon/maafw/runner/RunPlanPayload.kt b/app/src/main/java/com/aliothmoon/maafw/runner/RunPlanPayload.kt index d942b2aa..f522cea1 100644 --- a/app/src/main/java/com/aliothmoon/maafw/runner/RunPlanPayload.kt +++ b/app/src/main/java/com/aliothmoon/maafw/runner/RunPlanPayload.kt @@ -23,7 +23,7 @@ data class RunPlanPayload( val displayMode: Int = DisplayMode.BACKGROUND, /** * 后台模式 StartApp 前是否无条件强停目标应用。 - * 线格式默认 true 是为了旧 payload 解码后保持既有行为;用户设置本身默认 false。 + * 线格式默认 true 是为了旧 payload 解码后保持既有行为;用户设置本身默认 true。 */ val forceRestartApp: Boolean = true, val tasks: List, diff --git a/app/src/main/java/com/aliothmoon/maafw/session/SessionContracts.kt b/app/src/main/java/com/aliothmoon/maafw/session/SessionContracts.kt index d910011d..219f36bc 100644 --- a/app/src/main/java/com/aliothmoon/maafw/session/SessionContracts.kt +++ b/app/src/main/java/com/aliothmoon/maafw/session/SessionContracts.kt @@ -64,7 +64,7 @@ data class SessionUiState( val closeAppAfterTask: Boolean = false, val touchPreviewEnabled: Boolean = true, /** 后台模式 StartApp 前是否无条件强停目标应用 */ - val forceRestartApp: Boolean = false, + val forceRestartApp: Boolean = true, /** 定时任务的亮屏解锁;逐条规则的收尾选项在 ScheduleStrategy 上,不在这 */ val wakeUnlockEnabled: Boolean = false, val wakeCredential: String = "", diff --git a/app/src/main/java/com/aliothmoon/maafw/session/SessionViewModel.kt b/app/src/main/java/com/aliothmoon/maafw/session/SessionViewModel.kt index 770a0aed..c6b41192 100644 --- a/app/src/main/java/com/aliothmoon/maafw/session/SessionViewModel.kt +++ b/app/src/main/java/com/aliothmoon/maafw/session/SessionViewModel.kt @@ -72,7 +72,7 @@ private data class SettingsSnapshot( val resolutionPreference: ResolutionPreference, val debugMode: Boolean, val themeStyle: ThemeStyle = ThemeStyle.DEFAULT, - val forceRestartApp: Boolean = false, + val forceRestartApp: Boolean = true, val env: EnvSnapshot = EnvSnapshot(), val quick: QuickSnapshot = QuickSnapshot(), ) diff --git a/app/src/main/java/com/aliothmoon/maafw/settings/AppSettings.kt b/app/src/main/java/com/aliothmoon/maafw/settings/AppSettings.kt index 843b09d5..b05baf35 100644 --- a/app/src/main/java/com/aliothmoon/maafw/settings/AppSettings.kt +++ b/app/src/main/java/com/aliothmoon/maafw/settings/AppSettings.kt @@ -58,9 +58,9 @@ data class AppSettings( @PrefKey(default = "true") val touchPreviewEnabled: String = "true", - /** 后台模式下 StartApp 前是否无条件强停目标应用;默认关,已在当前虚拟屏时保留现场 */ - @PrefKey(default = "false") - val forceRestartApp: String = "false", + /** 后台模式下 StartApp 前是否无条件强停目标应用;默认开,保持既有强停行为 */ + @PrefKey(default = "true") + val forceRestartApp: String = "true", /** [com.aliothmoon.maafw.runner.ResolutionPreference] 的 name */ @PrefKey(default = "P720") diff --git a/app/src/test/java/com/aliothmoon/maafw/session/SessionViewModelTest.kt b/app/src/test/java/com/aliothmoon/maafw/session/SessionViewModelTest.kt index 1abf245b..7d059011 100644 --- a/app/src/test/java/com/aliothmoon/maafw/session/SessionViewModelTest.kt +++ b/app/src/test/java/com/aliothmoon/maafw/session/SessionViewModelTest.kt @@ -573,13 +573,13 @@ class SessionViewModelTest { backgroundScope.launch { vm.uiState.collect {} } advanceUntilIdle() - assertFalse(vm.uiState.value.forceRestartApp) + assertTrue(vm.uiState.value.forceRestartApp) - vm.onIntent(SessionIntent.SetForceRestartApp(true)) + vm.onIntent(SessionIntent.SetForceRestartApp(false)) advanceUntilIdle() - assertTrue(settings.forceRestartApp.value) - assertTrue(vm.uiState.value.forceRestartApp) + assertFalse(settings.forceRestartApp.value) + assertFalse(vm.uiState.value.forceRestartApp) } @Test diff --git a/app/src/test/java/com/aliothmoon/maafw/settings/FakeAppSettingsGateway.kt b/app/src/test/java/com/aliothmoon/maafw/settings/FakeAppSettingsGateway.kt index d2e25ba9..e56aff48 100644 --- a/app/src/test/java/com/aliothmoon/maafw/settings/FakeAppSettingsGateway.kt +++ b/app/src/test/java/com/aliothmoon/maafw/settings/FakeAppSettingsGateway.kt @@ -40,7 +40,7 @@ class FakeAppSettingsGateway : AppSettingsGateway { touchPreviewEnabled.value = enabled } - override val forceRestartApp = MutableStateFlow(false) + override val forceRestartApp = MutableStateFlow(true) override suspend fun setForceRestartApp(enabled: Boolean) { forceRestartApp.value = enabled