Files
android_packages_apps_Pawle…/src/os/pawlet/profiled/payloads/FirstBootHandler.kt
T
oxmc a48077078c Initial commit: PawletProfiled, Android device-owner priv-app
Android counterpart to git.oxmc.me/PawletOS/profiled's Linux daemon --
independent implementation, not a port. Same .vconfig profile format and
17 payload types, but implemented against DevicePolicyManager/VpnManager/
WifiManager/WallpaperManager/KeyChain instead of a native NDK Binder
daemon, since that's where AOSP actually exposes this functionality.

Self-provisions as device owner at first boot (DeviceOwnerProvisioner.kt)
to unlock the DevicePolicyManager-gated payload types (cert, pkcs12,
passcode, proxy, screensaver lock enforcement). 16 of 17 payload types are
real implementations; firewall is a documented platform dead end (no app
UID gets CAP_NET_ADMIN). See README's capability matrix for the full
per-payload breakdown.

Reviewed against the documented @SystemApi/hidden-API surface, not
compiled -- no AOSP toolchain available in this environment.
2026-07-25 01:35:58 -07:00

62 lines
2.8 KiB
Kotlin

package os.pawlet.profiled.payloads
import android.content.Context
import android.provider.Settings
import android.util.Log
import os.pawlet.profiled.Fields
import os.pawlet.profiled.ParsedPayload
import java.io.File
// Android counterpart to platform/linux/FirstBoot.cpp. Same "skip" field,
// but Android's Setup Wizard skip mechanism is all-or-nothing rather than
// the pane-by-pane cloud-init module list the Linux side builds:
// Settings.Global.DEVICE_PROVISIONED + Settings.Secure.USER_SETUP_COMPLETE
// suppress the entire wizard in one step (the standard mechanism a
// pre-provisioned device-owner build uses instead of the interactive
// NFC/QR managed-provisioning flow). So this handler doesn't need — or
// have — a per-pane skip map the way FirstBoot.cpp's kSkipMap does; a
// non-empty "skip" list is honored by suppressing SUW entirely, same
// practical outcome as the Linux side's more granular approach.
class FirstBootHandler : PayloadHandler {
companion object {
private const val TAG = "PawletProfiled/FirstBoot"
const val DONE_STAMP = "/data/system/pawletos/firstboot.done"
}
override fun apply(context: Context, payload: ParsedPayload): Boolean {
val skipJson = Fields.of(payload, "skip")
return try {
Settings.Global.putInt(context.contentResolver, Settings.Global.DEVICE_PROVISIONED, 1)
Settings.Secure.putInt(context.contentResolver, "user_setup_complete", 1)
if (skipJson.contains("software-update")) {
// Mirrors what the schema's software-update payload would set;
// if this profile only lists it under first-boot's skip array
// without its own software-update payload, still suppress
// BgUpd's install-time nag by writing the same bridge file
// SoftwareUpdateHandler uses.
File(SoftwareUpdateHandler.POLICY_PATH).apply { parentFile?.mkdirs() }
.writeText("""{"autoCheck":true,"autoDownload":true,"autoInstall":false,"deferDays":0}""")
}
File(DONE_STAMP).apply { parentFile?.mkdirs() }.writeText(payload.uuid)
Log.i(TAG, "suppressed Setup Wizard, stamp written")
true
} catch (e: Exception) {
Log.e(TAG, "failed to apply first-boot config", e)
false
}
}
override fun revert(context: Context, payload: ParsedPayload) {
// Deliberately not un-suppressing Setup Wizard on revert — undoing
// "the device already went through first boot" isn't a meaningful
// or safe operation once real accounts/data exist. Only the stamp
// (an internal bookkeeping file, not user-facing state) is cleared.
File(DONE_STAMP).delete()
Log.i(TAG, "cleared first-boot stamp uuid=${payload.uuid}")
}
}