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.
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 blackView,FLAG_KEEP_SCREEN_ON,supportActionBar?.hide()) → runs the configured command viaProcessBuilder("sh", "-c", command), output logged tofilesDir/workload.log. The app does not auto-wrap the command insu— the user includessu -c '...'themselves in Settings.SettingsActivity.kt— countdown seconds + command line, persisted inSharedPreferences(slocker_launcher_prefs), defaults60/sleep 180.BootReceiver.kt— onBOOT_COMPLETED, startsKioskBootService.KioskBootService.kt— foreground service (foregroundServiceType="specialUse") that posts a full-screen-intent notification (IMPORTANCE_HIGHchannel) targetingMainActivity, thenstopSelf()s after a short delay. This is deliberately not a plainstartActivity()call from the receiver/service — see the note below on why.MainActivity.onCreate()also callssetShowWhenLocked(true)+setTurnScreenOn(true)(API 27+) so it can display over the lock screen, and opportunistically requestsPOST_NOTIFICATIONSat 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 callablesu, but itssuisroot:shell-only (mode 750), so it can't validate app-invokedsu.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 →suvia Magisk →bwrap→tmux→sshd) 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:
- Calling
startActivity()straight fromBootReceivergets logged byActivityTaskManageras a blocked "Background activity start" and the process gets killed bylmkd~2s later, before ever becoming visible. - Routing through a foreground service (
KioskBootService) is necessary but not sufficient — on this device/OS,startActivity()from a genuinely-FOREGROUND_SERVICEprocess was still logged as a blocked background start. - 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) andPOST_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.