Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/src/main/java/com/aliothmoon/maafw/di/RunnerModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ val runnerModule = module {
nativeLibraryDir = context.applicationInfo.nativeLibraryDir,
runMode = get<AppSettingsManager>().runMode::value,
resolutionPreference = get<AppSettingsManager>().resolutionPreference::value,
forceRestartApp = get<AppSettingsManager>().forceRestartApp::value,
debugMode = get<AppSettingsManager>().debugMode::value,
scope = get(named<AppCoroutineScope>()),
servicePort = get(),
Expand Down Expand Up @@ -137,4 +138,4 @@ val runnerModule = module {
journal = get(),
)
}
}
}
6 changes: 4 additions & 2 deletions app/src/main/java/com/aliothmoon/maafw/remote/MaaRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.aliothmoon.maafw.maa.MaaFrameworkLoader
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
Expand Down Expand Up @@ -288,6 +289,7 @@ class MaaRunner(private val agentHost: AgentHost) {
return "libbridge.so 未加载,无法建立 native controller"
}

ActivityUtils.forceRestartOnVirtualDisplay = payload.forceRestartApp
val displayId = when (payload.displayMode) {
DisplayMode.PRIMARY ->
if (PrimaryDisplayManager.getCaptureSize() == null) {
Expand Down Expand Up @@ -480,8 +482,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) }

Expand Down Expand Up @@ -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()}")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -324,6 +326,7 @@ class MaaFrameworkRunnerPort(
screenWidth = width,
screenHeight = height,
displayMode = mode.displayMode,
forceRestartApp = forceRestartApp(),
tasks = plan.tasks.map {
RuntimeTaskPayload(
taskName = it.taskName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ data class RunPlanPayload(
val screenHeight: Int,
/** [com.aliothmoon.maafw.constant.DisplayMode] 取值;决定 Runner 找谁要屏幕尺寸 */
val displayMode: Int = DisplayMode.BACKGROUND,
/**
* 后台模式 StartApp 前是否无条件强停目标应用。
* 线格式默认 true 是为了旧 payload 解码后保持既有行为;用户设置本身默认 true。
*/
val forceRestartApp: Boolean = true,
val tasks: List<RuntimeTaskPayload>,
/** PI 声明的 agent,按声明顺序;空表示本次不起 agent */
val agents: List<AgentPayload> = emptyList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ data class SessionUiState(
/** 全局的跑完关目标应用;与 ScheduleStrategy 上的同名选项并存,本项优先 */
val closeAppAfterTask: Boolean = false,
val touchPreviewEnabled: Boolean = true,
/** 后台模式 StartApp 前是否无条件强停目标应用 */
val forceRestartApp: Boolean = true,
/** 定时任务的亮屏解锁;逐条规则的收尾选项在 ScheduleStrategy 上,不在这 */
val wakeUnlockEnabled: Boolean = false,
val wakeCredential: String = "",
Expand Down Expand Up @@ -257,6 +259,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

/** 立刻关掉目标应用:停虚拟屏,屏上的应用跟着一起没 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ private data class SettingsSnapshot(
val resolutionPreference: ResolutionPreference,
val debugMode: Boolean,
val themeStyle: ThemeStyle = ThemeStyle.DEFAULT,
val forceRestartApp: Boolean = true,
val env: EnvSnapshot = EnvSnapshot(),
val quick: QuickSnapshot = QuickSnapshot(),
)
Expand Down Expand Up @@ -140,6 +141,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) }
Expand Down Expand Up @@ -270,6 +273,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,
Expand Down Expand Up @@ -485,6 +489,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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ data class AppSettings(
@PrefKey(default = "true")
val touchPreviewEnabled: String = "true",

/** 后台模式下 StartApp 前是否无条件强停目标应用;默认开,保持既有强停行为 */
@PrefKey(default = "true")
val forceRestartApp: String = "true",

/** [com.aliothmoon.maafw.runner.ResolutionPreference] 的 name */
@PrefKey(default = "P720")
val resolutionPreference: String = "P720",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ interface AppSettingsGateway {
val touchPreviewEnabled: StateFlow<Boolean>
suspend fun setTouchPreviewEnabled(enabled: Boolean)

/** 后台模式 StartApp 前是否无条件强停目标应用 */
val forceRestartApp: StateFlow<Boolean>
suspend fun setForceRestartApp(enabled: Boolean)

val resolutionPreference: StateFlow<ResolutionPreference>
suspend fun setResolutionPreference(preference: ResolutionPreference)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ class AppSettingsManager(private val context: Context) : AppSettingsGateway {
private val _touchPreviewEnabled = MutableStateFlow(defaults.touchPreviewEnabled.toBoolean())
override val touchPreviewEnabled: StateFlow<Boolean> = _touchPreviewEnabled.asStateFlow()

private val _forceRestartApp = MutableStateFlow(defaults.forceRestartApp.toBoolean())
override val forceRestartApp: StateFlow<Boolean> = _forceRestartApp.asStateFlow()

private val _eventNotificationLevel =
MutableStateFlow(parseEventNotificationLevel(defaults.eventNotificationLevel))
val eventNotificationLevel: StateFlow<EventNotificationLevel> = _eventNotificationLevel.asStateFlow()
Expand Down Expand Up @@ -135,6 +138,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)
Expand Down Expand Up @@ -190,6 +194,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 }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,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,
)
if (PipController.isSupported(LocalContext.current)) {
Spacer(Modifier.height(MaaDesignTokens.Spacing.sm))
MaaSwitchRow(
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values-en/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@
<string name="settings_run_mode">Run mode</string>
<string name="settings_run_mode_background">Background mode</string>
<string name="settings_run_mode_foreground">Foreground mode</string>
<string name="settings_run_mode_hint_background">Runs on a separate virtual display so the phone stays usable; resolution derived from the project declaration</string>
<string name="settings_run_mode_hint_foreground">Drives the current screen directly, occupying the phone while running; the target app is not restarted and preview touch is disabled</string>
<string name="settings_force_restart_app">Force-restart the target app</string>
<string name="settings_force_restart_app_desc">Try enabling this when the target app cannot start normally</string>
<string name="settings_resource">Server</string>
<string name="settings_no_resources">The project provides no selectable servers</string>
<string name="settings_reload_project">Reload project</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@
<string name="settings_run_mode">运行模式</string>
<string name="settings_run_mode_background">后台模式</string>
<string name="settings_run_mode_foreground">前台模式</string>
<string name="settings_run_mode_hint_background">在独立的虚拟屏幕上运行任务,手机可正常使用;分辨率由项目声明推导</string>
<string name="settings_run_mode_hint_foreground">直接操作当前屏幕,运行期间手机被占用;不会重启目标应用,预览里也不接管触摸</string>
<string name="settings_force_restart_app">强制重启目标应用</string>
<string name="settings_force_restart_app_desc">无法正常启动目标app时可尝试开启此项</string>
<string name="settings_resource">服务器</string>
<string name="settings_no_resources">项目未提供可选服务器</string>
<string name="settings_reload_project">重新加载项目</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ open class FakePrivilegedService : RemoteService {
var running: Boolean = false
var setupResult: Boolean = true
var startRunResult: Boolean = true
var startRunJsons: MutableList<String> = mutableListOf()
private set
var stopRunCount: Int = 0
private set

Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
),
)
}
}
Loading