From 435f8cc839eaee3690eeda008138511d1a582d0f Mon Sep 17 00:00:00 2001 From: toorux Date: Tue, 6 Oct 2026 22:25:04 +0800 Subject: [PATCH] Reuse running APK sessions and preserve input after launch failure --- docs/user-guide/apk-manager.md | 2 +- docs/zh-CN/user-guide/apk-manager.md | 2 ++ src/apk.rs | 35 ++++++++++++++++++++++++---- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/user-guide/apk-manager.md b/docs/user-guide/apk-manager.md index 3dbd70a..d0d97e9 100644 --- a/docs/user-guide/apk-manager.md +++ b/docs/user-guide/apk-manager.md @@ -28,7 +28,7 @@ Framely installs packages into separate Android package directories even when th New apps default to independent containers. Existing sideloaded containers with readable application state can be reused, including containers with a single APK mount. Additional apps are installed by Android's package manager into the container's persistent data layer; the original APK mount is preserved. Steam-managed containers are excluded. Lepton recommends one application per container because Android apps do not have the usual enforced isolation inside its containers. -Installation runs in a headless background container without opening the Android desktop, regardless of the selected display mode. After installation, choose **Open** if desired; installation does not automatically launch the APK. An app with no detected launch entry remains manageable but does not appear in the launcher. +Installation runs in a headless background container without opening the Android desktop, regardless of the selected display mode. After installation, choose **Open** if desired; installation does not automatically launch the APK. An app with no detected launch entry remains manageable but does not appear in the launcher. Opening an already visible app reuses its running container and virtual gamepad. A headless or fully hidden display, a physical window-size change, or first attaching a new gamepad requires a restart because Lepton cannot safely perform those display or device transitions in place. To update, open the app's management dialog and choose **Install newer APK**. Package identity and version are checked; Android enforces signing compatibility during replacement. Downgrades are refused, and signature failures never trigger an uninstall/reinstall workaround. Same-package replacement retains data. Updating may stop the target container, affecting its other applications. diff --git a/docs/zh-CN/user-guide/apk-manager.md b/docs/zh-CN/user-guide/apk-manager.md index ad0ba7f..fcb3c32 100644 --- a/docs/zh-CN/user-guide/apk-manager.md +++ b/docs/zh-CN/user-guide/apk-manager.md @@ -104,3 +104,5 @@ Moonlight V+ 12.12.12 的创建连接界面在当前测试的 Lepton 版本上 开启此选项后,请通过管理面板或启动台打开 APK。一次性命令行进程无法管理持续运行的输入桥接,此时命令行启动会明确报错。 共享容器中的 APK 使用各自独立的 Android 安装目录。容器重启会保留 Android 外部存储中的已下载资源;旧版单 APK 挂载会在保存的安装包可用时恢复,旧 OBB 资源链接会在替换安装前迁移,并保留原目标。这只能防止后续丢失,旧版启动流程已经删除的下载资源无法仅靠 APK 还原。 + +已显示的应用再次打开时复用运行中的容器和虚拟手柄。无显示或已完全隐藏的窗口、改变窗口物理尺寸、首次挂载新的手柄设备时仍需重启,因为 Lepton 无法安全地在运行中完成这些显示或设备切换。 diff --git a/src/apk.rs b/src/apk.rs index 3fd7727..94df636 100644 --- a/src/apk.rs +++ b/src/apk.rs @@ -2189,9 +2189,10 @@ progress(json!({"phase":"starting"}));start(home,&c,Some(false),&log)?; &["exec", &name, "getprop", "waydroid.active_apps"], Some(&log), )?; - // A background install has no display. Also recreate a - // closed display through normal boot instead of the - // HWC transition that Gamescope rejects. + // Lepton's hidden/full-display transition can kill + // its display service on Gamescope. These display + // changes require recreation; a visible running + // application reuses its container and input device. let orientation = podman( &["exec", &name, "getprop", "framely.window_orientation"], Some(&log), @@ -2249,7 +2250,7 @@ progress(json!({"phase":"starting"}));start(home,&c,Some(false),&log)?; Ok(()) })(); if launch_result.is_err() { - crate::gamepad::stop_context(&c.name); + crate::gamepad::stop_app(&c.name, &a.metadata.package); } launch_result?; } @@ -2949,7 +2950,13 @@ case "$1" in elif [ "$3" = pidof ]; then if [ -f '{dead}' ] || {{ [ "$4" = com.example.app ] && [ -f '{custom}' ]; }} || {{ [ "$4" = surfaceflinger ] && [ -f '{headless}' ]; }}; then exit 1; else echo 1050; fi; elif [ "$3" = dumpsys ]; then if [ -f '{custom}' ]; then echo 'packageList={{com.example.app}}'; fi; - elif [ "$3" = getprop ]; then echo 30; + elif [ "$3" = getprop ]; then + case "$4" in + waydroid.active_apps) if [ -f '{closed}' ]; then echo none; else echo Waydroid; fi;; + lepton.headless) echo false;; + framely.window_orientation) echo auto;; + *) echo 30;; + esac elif [ "$3" = cmd ]; then echo com.example.app/.Main; elif [ "$3" = am ]; then if [ -f '{launch_fail}' ]; then echo 'Error: launch failed'; else echo 'Status: ok'; fi; fi;; @@ -2961,6 +2968,7 @@ esac dead = dir.path().join("dead-process").display(), custom = dir.path().join("custom-process").display(), headless = dir.path().join("headless-display").display(), + closed = dir.path().join("closed-window").display(), incoming = dir.path().join("incoming.apk").display(), apk = baked.join("app_overlay/base.apk").display(), fail = dir.path().join("fail").display(), @@ -3097,6 +3105,23 @@ echo true > '{}' .contains("Lepton exited shortly after launch")); } #[test] + fn launching_an_already_visible_app_reuses_running_container() { + let f = Fixture::new(); + list(&f.home).unwrap(); + let id = "test/com.example.app"; + let media = f.home.join(".local/share/lepton/contexts/test/baked/data_overlay/media/0/Android/data/com.example.app/files/update.part"); + fs::create_dir_all(media.parent().unwrap()).unwrap(); + fs::write(&media, "partial download").unwrap(); + f.operation("launch", json!({"app":id})).unwrap(); + let trace = fs::read_to_string(f.dir.path().join("commands")).unwrap(); + assert!( + !trace.lines().any(|line| line.starts_with("stop ")), + "{trace}" + ); + assert!(trace.contains("setprop waydroid.active_apps Waydroid")); + assert_eq!(fs::read_to_string(media).unwrap(), "partial download"); + } + #[test] fn launch_health_supports_custom_processes_and_headless_vr() { let f = Fixture::new(); let db = load(&f.home).unwrap();