pawlet_config_apk
Configuration APK for PawletOS — an Android 16 custom ROM for Raspberry Pi 4/5.
This APK is installed at first boot by ConfigProvisioner, which downloads it from the release URL in vendor.cfg, installs it, then reads its resources to apply system settings and APN entries. When baked into the ROM as a system app it also acts as a Partner customization source for Lawnchair.
Building
./gradlew assembleRelease
# Output: build/outputs/apk/release/pawlet_config_apk-release.apk
The APK is signed with the debug key for standalone builds. When baked into the ROM, AOSP signs it with the platform key via Android.bp.
Upload a release to https://git.oxmc.me/PawletOS/config_apk/releases and set config_apk_url in vendor.cfg to point at it.
Settings — res/xml/settings.xml
ConfigProvisioner reads this file after installing the APK and applies each entry using WRITE_SECURE_SETTINGS. All settings are optional; comment out anything you don't need.
Setting types
<settings>
<secure name="key" value="value" /> <!-- Settings.Secure -->
<system name="key" value="value" /> <!-- Settings.System -->
<global name="key" value="value" /> <!-- Settings.Global -->
</settings>
Conditional blocks
Wrap any settings in <if> to apply them only when all listed conditions match. Nest <if> blocks for AND logic; use separate blocks at the same level for OR-like behaviour.
<if [conditions…]>
<secure name="…" value="…" />
</if>
| Attribute | Match type | Example |
|---|---|---|
manufacturer |
exact, case-insensitive | manufacturer="motorola" |
brand |
exact, case-insensitive | brand="motorola" |
model |
substring, case-insensitive | model="Raspberry Pi 5" |
sdk |
exact API level | sdk="36" |
sdk_min |
minimum API level (inclusive) | sdk_min="31" |
sdk_max |
maximum API level (inclusive) | sdk_max="34" |
form_factor |
tablet | phone | flip | tv |
form_factor="tablet" |
feature |
PackageManager feature string | feature="android.hardware.telephony" |
Telephony defaults
The telephony block in settings.xml is gated on android.hardware.telephony and sets safe defaults (mobile data on, roaming off, LTE preferred, VoLTE on). Edit the values directly — the <if> wrapper means they are silently skipped on devices without a SIM slot (e.g. RPi without a modem hat).
APNs
ConfigProvisioner applies APNs using WRITE_APN_SETTINGS after installing the APK. Two modes are supported; the multi-file mode takes precedence.
Mode 1 — Multi-file assets/apns/ (preferred)
Place XML files anywhere under assets/apns/. ConfigProvisioner walks the directory recursively and parses every .xml it finds. Files can be organised however you like:
assets/apns/
global.xml # hand-authored carrier-agnostic entries
assets/apns/lineage/ # auto-populated from LineageOS submodule (see below)
US.xml
GB.xml
…
Files must use the standard AOSP / LineageOS/android_vendor_apn format:
<apns version="8">
<apn carrier="Carrier Name"
mcc="310" mnc="260"
apn="fast.t-mobile.com"
type="default,supl"
protocol="IPV4V6"
roaming_protocol="IPV4V6"
carrier_enabled="true" />
</apns>
Both carrier= (AOSP) and name= (custom) are accepted for the display name. Numeric is derived from mcc+mnc if a numeric= attribute is absent.
Supported attributes: carrier/name, mcc, mnc, apn, type, protocol, roaming_protocol, server, proxy, port, mmsc, mmsproxy, mmsport, user, password, authtype, bearer_bitmask, profile_id, mtu, modem_cognitive, max_conns, wait_time, max_conns_time, carrier_enabled, mvno_type, mvno_match_data
MVNO entries (same MCC/MNC, different mvno_type/mvno_match_data) are treated as distinct APNs. Deduplication key: numeric + apn + mvno_type + mvno_match_data.
Using LineageOS/android_vendor_apn as a submodule
The submodule goes at the repo root, not inside assets/. Gradle copies only the *.xml files into the APK, leaving scripts, schemas, and licence files behind.
git submodule add https://github.com/LineageOS/android_vendor_apn lineage_apn
git submodule update --init
build.gradle automatically copies lineage_apn/**/*.xml → build/generated/assets/apns/lineage/ before the asset merge step. If the submodule is not initialised the copy task is a no-op — the build still succeeds.
To add another APN source, add one entry to apnSubmodules in build.gradle:
def apnSubmodules = [
'lineage_apn': 'lineage',
'my_custom_apn': 'custom', // git submodule add <url> my_custom_apn
]
Mode 2 — Single-file res/xml/apns.xml (fallback)
Used only when assets/apns/ is absent or empty. Same APN attribute set as above, plus a numeric= attribute as an alternative to separate mcc/mnc.
<apns>
<apn name="My Carrier"
numeric="31026"
mcc="310" mnc="26"
apn="internet"
type="default,supl"
protocol="IPV4V6"
roaming_protocol="IPV4V6"
carrier_enabled="1" />
</apns>
Launcher layout
Two paths exist depending on whether the APK is a system app or a user app.
Path A — Partner customization (system app only)
When the APK is baked into the ROM, Lawnchair discovers it via MATCH_SYSTEM_ONLY and reads res/xml/partner_default_layout.xml directly. Edit that file to set the default home screen.
Path B — ContentProvider (user app, installed by ConfigProvisioner)
ConfigProvisioner writes Settings.Secure["launcher3.layout.provider"] = "app.pawlet.config.layout". Lawnchair then requests the layout from LayoutProvider, which serves res/raw/partner_default_layout.xml as raw bytes.
Edit res/raw/partner_default_layout.xml for the ContentProvider path (the file in res/xml/ is for the system-app path). Both files exist separately because res/xml/ is AAPT2-compiled to binary while res/raw/ is copied as-is.
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto">
<favorite
launcher:packageName="com.vivaldi.browser"
launcher:className="com.vivaldi.browser.ChromeTabbedActivity"
launcher:screen="0" launcher:x="0" launcher:y="0" />
</favorites>
Icon pack — res/xml/appfilter.xml
Standard ADW/Nova/Lawnchair icon pack format. Map component names to drawable resource names:
<resources>
<item component="ComponentInfo{com.example.app/com.example.app.MainActivity}"
drawable="ic_example" />
</resources>
Place the corresponding drawables in res/drawable-nodpi/. The IconPackActivity is a no-display activity that satisfies launcher icon pack discovery queries.
ROM integration (system app path)
Android.bp
android_app {
name: "PawletConfig",
srcs: [],
resource_dirs: ["res"],
certificate: "platform",
sdk_version: "current",
privileged: false,
optimize: { enabled: false },
}
Product makefile
Add to pawlet_rpi4.mk and pawlet_rpi5.mk:
PRODUCT_PACKAGES += PawletConfig
Local manifest
Add to android_local_manifest/pawletos.xml:
<project name="PawletOS/config_apk"
path="vendor/pawlet/config_apk"
remote="pawlet"
revision="master" />
ConfigProvisioner integration
ConfigProvisioner (dev.oxmc.configprovisioner) is the system app that drives this APK. Relevant vendor.cfg keys:
| Key | Default | Description |
|---|---|---|
config_apk_url |
— | HTTPS URL to download the APK from |
config_apk_package |
app.pawlet.config |
Package name to read settings from after install |
After a successful install ConfigProvisioner:
- Reads
res/xml/settings.xmlfrom the installed package and applies each entry viaSettings.Secure/System/Global.putString() - Reads APNs from
assets/apns/(orres/xml/apns.xml) and inserts intocontent://telephony/carriers
Both steps are no-ops if the relevant resources are absent or empty.
Device Owner (optional, currently disabled)
PawletDeviceAdminReceiver is declared but not activated automatically. Activation requires an explicit adb command on a freshly wiped device before any accounts are added:
adb shell dpm set-device-owner app.pawlet.config/.PawletDeviceAdminReceiver
When active as Device Owner the app can push managed configurations to other apps via DevicePolicyManager.setApplicationRestrictions(). The relevant code in ConfigReceiver.kt is commented out pending this being needed.
Android API compatibility
| API level | Android version | Notes |
|---|---|---|
| 36 | Android 16 | Primary target (PawletOS) |
| 29 | Android 10 | Minimum (minSdk) |