Files
ceamac bf91d8ec52 Fix unattended boot auto-launch
Plain startActivity() from BootReceiver (and even from a foreground
service) gets blocked as a background activity start and the process
gets reclaimed before becoming visible. Switch to a full-screen-intent
notification (the mechanism alarm/call apps use), which needs
USE_FULL_SCREEN_INTENT + POST_NOTIFICATIONS granted, plus
setShowWhenLocked/setTurnScreenOn on MainActivity so it displays over
the lock screen. Confirmed working on an untouched real-device reboot.
2026-09-14 06:05:50 +00:00

3.9 KiB

Slocker Launcher

Android app that turns a rooted phone into a headless container player: on boot it auto-launches, shows a skippable countdown, then takes over the screen (fullscreen, solid black, screen kept on) while running a user-configured shell command in the background — normally su -c '...' starting a bwrap chroot (Gentoo rootfs) that runs tmux + sshd, so the actual workload (e.g. slocker-lite) is reachable over SSH without ever plugging in a cable.

Kotlin, no native code. Package ro.ceamac.slockerlauncher, minSdk 23, targetSdk 37.

Architecture

  • MainActivity.kt — countdown screen (Start/Settings/Exit buttons) → kiosk transition (fullscreen black View, FLAG_KEEP_SCREEN_ON, supportActionBar?.hide()) → runs the configured command via ProcessBuilder("sh", "-c", command), output logged to filesDir/workload.log. The app does not auto-wrap the command in su — the user includes su -c '...' themselves in Settings.
  • SettingsActivity.kt — countdown seconds + command line, persisted in SharedPreferences (slocker_launcher_prefs), defaults 60 / sleep 180.
  • BootReceiver.kt — on BOOT_COMPLETED, starts KioskBootService.
  • KioskBootService.kt — foreground service (foregroundServiceType="specialUse") that posts a full-screen-intent notification (IMPORTANCE_HIGH channel) targeting MainActivity, then stopSelf()s after a short delay. This is deliberately not a plain startActivity() call from the receiver/service — see the note below on why.
  • MainActivity.onCreate() also calls setShowWhenLocked(true) + setTurnScreenOn(true) (API 27+) so it can display over the lock screen, and opportunistically requests POST_NOTIFICATIONS at runtime (needed for the full-screen intent above to fire on API 33+; can only be requested from an Activity, so this only takes effect starting the next boot after a manual open grants it).

Build & install

./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Debug-signed APK is fine for sideloading; no release signing config exists.

Test devices

  • Emulator: AVD Medium_Phone (Google APIs non-Play, x86_64, google_apis_ps16k, Android 17/API 37) — non-Play images ship a callable su, but its su is root:shell-only (mode 750), so it can't validate app-invoked su. ANDROID_HOME=~/Android/Sdk, kvm group membership required for acceleration.
  • Real device: Samsung Galaxy A71 (SM-A715F, Android 13, rooted via Magisk) — the actual deployment target. Full pipeline (boot → kiosk → su via Magisk → bwraptmuxsshd) verified working here.

Why boot-launch works the way it does

Getting auto-launch to actually run unattended after a real (not manually-touched) reboot took three iterations, each diagnosed from adb logcat -d | grep -i slockerlauncher after an untouched reboot — worth reading before changing this path:

  1. Calling startActivity() straight from BootReceiver gets logged by ActivityTaskManager as a blocked "Background activity start" and the process gets killed by lmkd ~2s later, before ever becoming visible.
  2. Routing through a foreground service (KioskBootService) is necessary but not sufficient — on this device/OS, startActivity() from a genuinely-FOREGROUND_SERVICE process was still logged as a blocked background start.
  3. The mechanism Android actually exempts for this is a full-screen-intent notification (same one alarm/call apps use) — needs USE_FULL_SCREEN_INTENT (manifest, normal/auto-granted) and POST_NOTIFICATIONS (runtime, API 33+) both granted, or the intent silently never fires.

Confirmed working end to end on the real device (Galaxy A71, Android 13): untouched reboot → full-screen-intent notification → MainActivity resumes on its own → kiosk black screen → configured su -c command.