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.
PawletProfiled
PawletOS's Android device management app — a platform-signed priv-app and
device owner. Android counterpart to
pawletprofiled, the Linux
daemon: same .vconfig profile format, same payload types, same
os.pawlet.profiled AIDL contract — but an independent implementation,
not shared code. Most of what these payloads need on Android
(DevicePolicyManager, VpnManager, WifiManager, WallpaperManager,
KeyChain) is Java-SDK-first and not cleanly reachable from a native
process, which is why this is an app instead of another NDK binder daemon.
Contents
- Why an app, not a daemon
- Device owner
- Payload capability matrix
- Architecture
- Third-party dependencies
- Building
- Package layout
Why an app, not a daemon
The Linux daemon is a native root process that owns the machine outright —
writing NetworkManager keyfiles, editing PAM/nftables config, calling
update-ca-certificates. Android doesn't expose an equivalent to any
app UID, root included: Wi-Fi, VPN, certificate installation, and
password policy all go through DevicePolicyManager/WifiManager/
VpnManager, which are Java APIs gated on device owner status, not
Linux-style file permissions. Standing up a native process just to
AIDL-call back into system_server for everything would be strictly
more code than calling those APIs directly — so this is a Kotlin app.
Device owner
DeviceOwnerProvisioner.kt self-provisions via
DevicePolicyManager.setDeviceOwner() — the same @SystemApi call
ManagedProvisioning itself uses — from BootCompletedReceiver, guarded by
isDeviceOwnerApp(). This only succeeds pre-SUW, before any account
exists, which a factory-fresh PawletOS image satisfies on first boot.
Every later boot's call is expected to fail and is treated as "already
provisioned," not an error.
Payload capability matrix
| Payload | Status | Notes |
|---|---|---|
wifi |
Real | WifiManager.addNetwork() (privileged path — the only one covering WEP + enterprise + static proxy in one object) |
ethernet |
Real (IP/proxy) | EthernetManager. 802.1X not applied — no stable cross-device @SystemApi for wired EAP; depends on the board's Ethernet HAL |
vpn |
Real (IKEv2 only) | VpnManager/Ikev2VpnProfile, PSK or username+password. L2TP: no AOSP client exists at all. Custom: no bundled tunnel backend, same as the Linux side's own placeholder |
cert |
Real | DevicePolicyManager.installCaCert() |
pkcs12 |
Real | Parsed locally via KeyStore("PKCS12"), installed via DevicePolicyManager.installKeyPair() |
passcode |
Real | DevicePolicyManager password-quality/history/lockout setters |
mdm |
Real | Device-owner status is the enrollment; this records server metadata for isDeviceManaged()/getMdmServerUrl() |
software-update |
Bridged | Writes a policy file for BgUpd to read — BgUpd doesn't read it yet, same "wired one side, not both" state as the Linux daemon's own comment about this integration |
time-server |
Real | Settings.Global.NTP_SERVER |
proxy |
Real | DevicePolicyManager.setRecommendedGlobalProxy(). No proxy-auth — ProxyInfo doesn't carry credentials |
dns-proxy |
Real (narrower) | Android Private DNS is DoT-hostname only, not arbitrary DoH URLs; the host is extracted from whatever URL the payload carries |
firewall |
Not applicable | No app UID gets CAP_NET_ADMIN, device owner included, and stock Android has no unsolicited-inbound-connection surface to protect. The one genuine dead end — not a missing library, missing kernel privilege |
ldap |
Real | Bundled UnboundID LDAP SDK (Android has no JNDI/system LDAP client at all) actually binds and searches at apply time. No OS-level directory-accounts sync (Android has nothing like macOS Open Directory) |
wallpaper |
Real | WallpaperManager. locked recorded, not enforced (no such API) |
screensaver |
Real | Daydream settings + DevicePolicyManager.setMaximumTimeToLock() for the actual security-relevant lock enforcement |
first-boot |
Real (coarser) | DEVICE_PROVISIONED/user_setup_complete skip the entire Setup Wizard in one step, vs. the Linux side's pane-by-pane cloud-init module list |
content-cache |
Real | Writes /data/misc/pawletcache/policy.json — PawletOS-specific, read by android_packages_apps_PawletCache's PolicyOverride.kt |
Architecture
BootCompletedReceiver ──▶ DeviceOwnerProvisioner (first boot only)
│ │
│ ▼
│ preinstalled/*.vconfig ──▶ PawletProfileApplication
│ │
└──▶ ZteLookupClient (enrollment) ─────────────────────┤
▼
IPawletProfileService.Stub (PawletProfileService) ──▶ PawletProfileApplication
│
┌─────────────────────┤
▼ ▼
SignatureVerifier ProfileParser
(Bouncy Castle CMS) (SnakeYAML)
│ │
└──────────┬──────────┘
▼
ProfileStore
(/data/system/pawletos/profiles)
│
▼
PayloadHandlerRegistry.forType()
──▶ one of 17 PayloadHandlers
PawletProfileApplication is the shared core (mirrors the Linux daemon's
PawletProfileService.cpp) — both the boot-time preinstalled-profile path
and the AIDL-facing bound-service path install through the same
installProfileDirect(), against the same ProfileStore, so there's one
source of truth regardless of which path a profile came in through.
Third-party dependencies
Not vendored as jars in this repo — pulled in via
maven-to-lib as
prebuilt_libs repos on the local manifest, same as every other Maven
dependency in the PawletOS tree. See the comment block at the top of
Android.bp for the exact config.yml entries. Three libraries, all pure
Java (no native/JNI component, no per-ABI split needed):
| Library | Fills the gap left by |
|---|---|
| SnakeYAML | No YAML parser anywhere in the Android platform/SDK |
Bouncy Castle (bcprov+bcpkix) |
BoringSSL has no CMS/PKCS#7 support — same gap the Linux daemon solves by vendoring a static OpenSSL, solved here with a pure-Java library instead |
| UnboundID LDAP SDK | Android has no javax.naming/JNDI and no system LDAP client |
Building
Not independently buildable outside an AOSP tree — this is a
platform_apis: true, certificate: "platform" priv-app.
cp -r android_packages_apps_PawletProfiled $AOSP_ROOT/vendor/oxmc/PawletProfiled/
echo 'PRODUCT_PACKAGES += PawletProfiled' >> device/oxmc/pawletos/pawletos.mk
echo 'BOARD_SEPOLICY_DIRS += vendor/oxmc/PawletProfiled/sepolicy' >> device/oxmc/pawletos/BoardConfig.mk
echo 'PRODUCT_COPY_FILES += vendor/oxmc/PawletProfiled/etc/privapp-permissions-os.pawlet.profiled.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/permissions/privapp-permissions-os.pawlet.profiled.xml' \
>> device/oxmc/pawletos/pawletos.mk
source build/envsetup.sh && lunch pawletos_arm64-userdebug
m PawletProfiled
Same disclaimer as the rest of this session's Android work: statically
reviewed against the documented @SystemApi/hidden-API surface, never
compiled — no AOSP toolchain available in this environment.
Package layout
android_packages_apps_PawletProfiled/
├── Android.bp
├── AndroidManifest.xml
├── aidl/os/pawlet/profiled/
│ └── IPawletProfileService.aidl
├── etc/privapp-permissions-os.pawlet.profiled.xml
├── res/
│ ├── values/strings.xml
│ └── xml/device_admin_receiver.xml
├── sepolicy/
│ ├── pawletprofiled.te
│ └── file_contexts
└── src/os/pawlet/profiled/
├── PawletProfileApplication.kt Shared core: install/remove/query, mirrors the Linux D-Bus service
├── PawletProfileService.kt Bound Service hosting IPawletProfileService.Stub
├── PawletDeviceAdminReceiver.kt DeviceAdminReceiver + MDM-enrolled wipe-on-disable
├── BootCompletedReceiver.kt Device-owner provisioning, preinstalled profiles, ZTE
├── DeviceOwnerProvisioner.kt
├── ProfileParser.kt SnakeYAML
├── ProfileStore.kt Same on-disk layout as ProfileStore.cpp
├── SignatureVerifier.kt Bouncy Castle CMS
├── ProfileModels.kt / Fields.kt
├── payloads/ 17 handlers, one per payload type
└── zte/
├── DeviceIdentity.kt / AttestationKeyHasher.kt
└── ZteLookupClient.kt