SetupWizard: hide + lock system UI during setup

Decouple the setup UI hiding from the managed kiosk lockdown:
- New WizardFlags.IMMERSIVE (default true): hide the status/nav bars during
  setup (sticky immersive) independent of KIOSK. KIOSK stays the aggressive
  managed lockdown (overlay window + non-sticky + future lock-task).
- BaseKioskActivity / MainActivity hide the bars when IMMERSIVE || KIOSK.
- Add StatusBarManager.setDisabledForSetup(true) on wizard start
  (EntryPoint + MainActivity) and setDisabledForSetup(false) in
  finishSetupWizard, so the shade/quick-settings/Home/Recents are actually
  locked during setup (matches LineageOS). Called via reflection so the
  module still builds under Gradle (the API is @SystemApi).
This commit is contained in:
oxmc
2026-07-18 02:20:18 -07:00
parent b328dbea85
commit f636cd1a2a
5 changed files with 65 additions and 10 deletions
@@ -15,6 +15,7 @@ import me.pawlet.setupwizard.lib.BaseKioskActivity
import me.pawlet.setupwizard.lib.SetupWizardManager
import me.pawlet.setupwizard.lib.WizardFlags
import me.pawlet.setupwizard.lib.buildBuiltinPages
import me.pawlet.setupwizard.lib.utils.SetupUtils
import me.pawlet.setupwizard.ui.screens.AboutDeviceScreen
import me.pawlet.setupwizard.ui.screens.AndroidVersionScreen
import me.pawlet.setupwizard.ui.theme.MainTheme
@@ -23,12 +24,20 @@ class MainActivity : BaseKioskActivity() {
private lateinit var wizardManager: SetupWizardManager
override fun onCreate(savedInstanceState: Bundle?) {
// Kiosk needs the overlay window type set before setContent; the actual
// bar-hiding (immersive or kiosk) is applied in onResume via
// BaseKioskActivity for both KIOSK and IMMERSIVE.
if (WizardFlags.KIOSK) {
FullScreenHelper.prepareKioskWindow(this)
FullScreenHelper.enableKioskMode(this)
}
super.onCreate(savedInstanceState)
// Lock the status bar (shade/home/recents) for the duration of setup;
// restored in SetupUtils.finishSetupWizard().
if (WizardFlags.KIOSK || WizardFlags.IMMERSIVE) {
SetupUtils().disableStatusBarForSetup(this)
}
wizardManager = SetupWizardManager.getInstance(this)
wizardManager.registerPages(
buildBuiltinPages(
@@ -46,7 +55,10 @@ class MainActivity : BaseKioskActivity() {
val deviceInfo by wizardManager.deviceInfo.collectAsState()
LaunchedEffect(index) {
if (WizardFlags.KIOSK) FullScreenHelper.enableKioskMode(this@MainActivity)
// Re-assert hidden system bars when the page changes.
if (WizardFlags.KIOSK || WizardFlags.IMMERSIVE) {
FullScreenHelper.onResume(this@MainActivity, WizardFlags.KIOSK)
}
}
when (overlay) {
@@ -15,6 +15,7 @@ import me.pawlet.setupwizard.lib.SetupWizardManager
import me.pawlet.setupwizard.lib.WizardFlags
import me.pawlet.setupwizard.lib.internal.PrefManager
import me.pawlet.setupwizard.lib.internal.UriHandler
import me.pawlet.setupwizard.lib.utils.SetupUtils
import me.pawlet.setupwizard.ui.screens.SplashScreen
import me.pawlet.setupwizard.ui.theme.MainTheme
import kotlinx.coroutines.runBlocking
@@ -41,6 +42,11 @@ class EntryPoint : BaseKioskActivity() {
super.onCreate(savedInstanceState)
// Lock the status bar for setup as early as the splash.
if (WizardFlags.KIOSK || WizardFlags.IMMERSIVE) {
SetupUtils().disableStatusBarForSetup(this)
}
// Initialize managers
prefs = PrefManager(this)
uriHandler = UriHandler(this)
@@ -6,14 +6,18 @@ import me.pawlet.setupwizard.lib.FullScreenHelper
open class BaseKioskActivity : ComponentActivity() {
override fun onResume() {
super.onResume()
// When kiosk is off we intentionally do NOT touch FullScreenHelper:
// onResume(_, false) still applies immersive mode (hides the bars), so
// the only way to show normal system bars is to skip the call entirely.
if (WizardFlags.KIOSK) FullScreenHelper.onResume(this, true)
// Hide the system bars during setup (immersive), or full kiosk when
// enabled. onResume(_, isKioskMode) hides the bars either way; the bool
// only picks sticky-immersive vs non-sticky kiosk style.
if (WizardFlags.KIOSK || WizardFlags.IMMERSIVE) {
FullScreenHelper.onResume(this, WizardFlags.KIOSK)
}
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (WizardFlags.KIOSK) FullScreenHelper.onWindowFocusChanged(this, hasFocus, true)
if (WizardFlags.KIOSK || WizardFlags.IMMERSIVE) {
FullScreenHelper.onWindowFocusChanged(this, hasFocus, WizardFlags.KIOSK)
}
}
}
@@ -6,12 +6,19 @@ package me.pawlet.setupwizard.lib
*/
object WizardFlags {
/**
* Kiosk / immersive lock-down (hides system bars, re-asserts on focus).
* Intended for managed/enrolled devices; disabled for the normal first-run
* experience so users can use the status bar and back gesture.
* Kiosk lock-down: overlay window type + non-sticky immersive + (future)
* lock-task, so the user cannot leave or reveal the bars. Intended for
* managed/enrolled devices. Off for the normal first-run experience.
*/
const val KIOSK = false
/**
* Hide the system bars (status + navigation) during setup — standard
* first-run behaviour, independent of the managed KIOSK lockdown. Uses
* sticky immersive when KIOSK is off. Turn off only for debugging.
*/
const val IMMERSIVE = true
/**
* Deep-link / URI entry points (oxn:// IPC scheme and http(s) VIEW links).
* Off for now — the wizard is launched only as the SETUP_WIZARD HOME app.
@@ -533,6 +533,29 @@ class SetupUtils {
}
}
/**
* Lock the status bar for setup: disables the notification shade / quick
* settings, Home, Recents, and assist so the user cannot leave setup. Uses
* the platform's setup API (StatusBarManager.setDisabledForSetup, a
* @SystemApi needing STATUS_BAR — which the wizard holds) via reflection so
* the module still builds under Gradle, where that API isn't on the SDK.
*/
fun disableStatusBarForSetup(context: Context) = setStatusBarDisabledForSetup(context, true)
/** Restore the status bar locked by [disableStatusBarForSetup]. */
fun enableStatusBar(context: Context) = setStatusBarDisabledForSetup(context, false)
private fun setStatusBarDisabledForSetup(context: Context, disabled: Boolean) {
try {
val sbm = context.getSystemService("statusbar") ?: return
Class.forName("android.app.StatusBarManager")
.getMethod("setDisabledForSetup", java.lang.Boolean.TYPE)
.invoke(sbm, disabled)
} catch (e: Exception) {
Log.w(TAG, "setDisabledForSetup($disabled) failed", e)
}
}
fun finishSetupWizard(context: Context) {
if (LOGV) {
Log.v(TAG, "finishSetupWizard")
@@ -566,6 +589,9 @@ class SetupUtils {
WallpaperManager.getInstance(context).forgetLoadedWallpaper()
disableHome(context)
// Restore the status bar that was locked for setup.
enableStatusBar(context)
// Notify partner/system apps (e.g. ConfigProvisioner) that setup is done
// so they can apply their own customizations. Guarded by FINISH_SETUP.
runCatching {