PicodroidManifest.xml reference
Every Picodroid app ships exactly one PicodroidManifest.xml at the root of its
directory. It declares the app’s package and names the single class the runtime
hands control to at launch.
A minimal manifest:
<?xml version="1.0" encoding="utf-8"?><manifest package="helloworld" version="1.0"> <application application="helloworld/HelloWorld" /></manifest>The build discovers the app, compiles its Java, and packs everything into a
.papk bundle (see Build). At runtime the firmware reads
the entry point out of the manifest and dispatches to it.
Schema
The manifest shape is validated at build time by the Gradle buildSrc parser
(ManifestSchema.kt). It fails the Gradle build immediately — at configuration
time — with a clear error rather than letting a malformed manifest reach the
packer.
The <manifest> root:
package(required, non-blank). The app’s identity: it becomes thepackage-namekey of the PAPK, which the device’s package directory keys on (one install per package; reinstalling a package replaces its previous copy). Missing or blank both fail. Reverse-DNS (com.acme.weather) and plain (helloworld) names are both accepted; the example apps use the plain form.version(optional, default"1.0"). The human-readable version name. Missing or blank falls back to"1.0". Any non-blank string is accepted verbatim; there is no format check.version-code(optional, default1). A positive integer that must grow with every release of the package — the package directory orders installs by it. Anything else fails the build.<application>(required). At least one must exist. Only the first<application>element is read; any extras are silently ignored. Besides the entry point (below), its optionalandroid:themeand its component children (see The Android form) it takes two optional identity attributes:label— the display name a launcher shows; defaults to the package name.icon— the file name of a PNG under the app’sassets/directory (see Bundled image assets). It must exist there, or the build fails at configuration time, and it should be a small square: 48×48 is the convention;papk-packwarns past 64×64 or when the sides differ.
<supports-screens design-width=".." design-height=".."/>(optional). The logical size the app lays out against; both attributes are positive integers and both are required when the element is present. The runtime shows the app at exactly that size on every panel. Absent: the app resizes to the panel. See Screens and features.<uses-feature name=".." required="true|false"/>(optional, repeatable). A hardware feature the app uses, by itsPackageManager.FEATURE_*name;requireddefaults tofalse. The installer refuses an app that requires a feature the board lacks.
The exact error strings, so you can grep this page if you hit one:
PicodroidManifest.xml not found: <absolute path><file>: expected <manifest> root, got <other-tag><file>: <manifest> missing 'package' attribute<file>: missing <application> element<file>: <application> must set exactly one of 'main-class', 'activity', or 'application'<file>: <application> sets multiple of 'main-class'/'activity'/'application' — pick one<file>: <manifest> version-code must be a positive integer, got '<value>'<app>: PicodroidManifest.xml icon="<name>" is not a file under assets/<file>: <supports-screens> design-width must be a positive integer, got '<value>'(anddesign-height)<file>: <uses-feature> missing 'name' attribute<file>: <uses-feature> required must be 'true' or 'false', got '<value>'<file>: <activity> missing 'android:name' attribute(and<service>)<file>: <activity> '<class>' is declared twice(and<service>)<file>: <activity> android:theme is not supported — there is one theme per app; put android:theme on <application><file>: two activities carry the MAIN intent filter ('<a>', '<b>'); one app has one entry<file>: <application> does not take <<tag>> (supported: <activity>, <service>, <uses-permission>)<file>: <application> android:theme must name a <style>, got '<value>'<file>: <application activity="<class>"> is not among the declared <activity> elements — declare it, or drop the attribute (…)PicodroidManifest.xml declares components, and the code starts some it does not declare (Android refuses these at run time; declare them under <application>):followed by the<service android:name=…/>/<activity …/>lines to add (theverifyManifesttask)manifest <service android:name="<class>"> is not among the packed classesand… does not extend picodroid.app.Service(and<activity>/Activity;papk-pack)the manifest names android:theme="@style/<Name>", and res/values defines no <style name="<Name>">(and… the app has no res/ tree to define it in)
DOCTYPE is rejected
The parser disables DTD processing entirely (disallow-doctype-decl). A
manifest containing a <!DOCTYPE …> declaration fails to parse — the XML
parser’s own exception propagates, there is no custom Picodroid message. This is
XXE / billion-laughs hardening: no external entities, no DTD. Just don’t put a
DOCTYPE in your manifest.
Editor validation (XSD)
sdk/PicodroidManifest.xsd mirrors the ManifestSchema.kt rules above —
required package, optional version and version-code, the exactly-one-of
main-class / activity / application entry point, and the optional label
and icon — for editor validation and autocomplete. It is not wired into the build; the Gradle parser remains the
authoritative check (the one-of rule is already enforced there), so the schema
can’t drift the build.
Referencing it is opt-in per file via xsi:noNamespaceSchemaLocation. The
helloworld example and the newApp template do so:
<?xml version="1.0" encoding="utf-8"?><manifest package="myapp" version="1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../sdk/PicodroidManifest.xsd"> <application application="myapp/MyApp" /></manifest>Manifests without the reference stay valid — the build never reads it. The one-of constraint uses an XSD 1.1 assertion, so the live red-squiggle for “two entry points set” needs a 1.1-aware validator (IntelliJ / Android Studio); other editors still get structure and attribute autocomplete.
Entry-point styles
The <application> element must set exactly one of three attributes, and
its value must be non-blank, or declare the entry as an <activity> child
(The Android form). Setting none, or more than one, is a
build error (see the strings above).
Every value is a JVM internal name in slash form: pkg/Class, never the
dotted pkg.Class. The leading segment is the package, separated from the class
name by /. (Nested packages use more slashes: com/example/foo/MyApp.) The
dotted form would not match the compiled class’s internal name and would fail at
runtime.
The Android form
The shape an Android manifest has, with what this platform reads of it (manifest-components-2026-10.md):
<manifest package="claudeusage" version="1.0"> <application android:theme="@style/AppTheme" label="Claude Usage"> <activity android:name=".ui.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".data.UsageService" /> </application></manifest><activity android:name="…">, repeatable. The one with theMAINintent filter is the entry; without one, the first declared. Names are read as Android reads them: dotted (claudeusage.ui.MainActivity), slash-form, or relative to the manifestpackagewith a leading dot. The short-formactivity=attribute may stay beside the children and must then name one of them.<service android:name="…">, repeatable. Declared components are checked twice at build time:papk-packchecks that each class is packed and extendspicodroid.app.Activity/Service, and the GradleverifyManifesttask checks that everynew Intent(X.class)the code starts names a declared component (every Service, and every Activity once activities are declared). Android refuses an undeclared component at run time; here the build does, with the element to add.Intent.setClassNameis dynamic and is not checked.android:theme="@style/Name"on<application>: the<style>inres/valuesthat?attr/…resolves against at build time and that the framework applies before the first Activity’sonCreate, as Android does, so the app does not callsetTheme. Without it, the style calledAppThemeserves?attr/and nothing is applied (styles and the theme). One theme per app: anandroid:themeon an<activity>is a build error.<uses-permission>is read and ignored. Nothing on this platform enforces a permission (WiFi, the network and the LEDs are open to every app); the element is accepted so an Android manifest’s lines do not break the build, and it grants nothing.<receiver>declares aBroadcastReceiverthe framework starts: for the actions its<intent-filter>names —BOOT_COMPLETED,TIME_SETandTIMEZONE_CHANGEDare the ones a manifest receiver may declare; the other system actions reach registered receivers only, and the build says so — and for an explicit broadcast Intent orPendingIntent.getBroadcastthat names the class. One app runs at a time, so a delivery to an app that is not running is a headless run of it:Application.onCreate, thenonReceivewith a plain Context, no Activity (one it starts is dropped with a log line); it happens at boot before the boot app, when an app hands over to the launcher, or by interrupting the launcher. The class must extendpicodroid.content.BroadcastReceiver.- Not read:
<meta-data>,<provider>, anything in an<intent-filter>but theMAINaction and a receiver’s actions. An unknown child of<application>fails the build naming what is supported. The bare spellingsname=andtheme=are accepted beside theandroid:ones.
A short-form manifest (activity= / application= / main-class= alone)
declares nothing and nothing is checked; that is every example but
claudeusage, layoutdemo, servicedemo and tutorial_service.
application= — full app with lifecycle
Use this for anything beyond a single screen: an app that starts Activities,
binds services, or needs the onCreate() setup hook. This is the form every
example app uses, and what newApp scaffolds.
<application application="myapp/MyApp" />package myapp;
import picodroid.app.Application;import picodroid.content.Intent;
public class MyApp extends Application { @Override public void onCreate() { startActivity(new Intent(this, MainActivity.class)); }}Application.onCreate() runs once at launch. From there you drive the UI by
launching Activities with startActivity(...). See
Multi-screen apps and
Background services.
activity= — single-screen UI
Use this when the whole app is one screen and you don’t need an Application
subclass. The runtime instantiates the named Activity and drives its lifecycle
directly.
<application activity="myapp/MainActivity" />package myapp;
import picodroid.app.Activity;import picodroid.os.Bundle;import picodroid.widget.TextView;
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView root = new TextView(); root.setText("Hello"); setContentView(root); }}See the UI API for building view trees.
main-class= — console / static main
Use this for a non-UI program — a console or benchmark-style app whose logic
lives in a static void main. The runtime invokes the static main method and
nothing else.
<application main-class="myapp/Main" />package myapp;
import picodroid.util.Log;
public class Main { public static void main(String[] args) { Log.i("Main", "Hello from main!"); }}App identity
A launcher lists installed apps by three values, all optional:
<?xml version="1.0" encoding="utf-8"?><manifest package="com.acme.weather" version="1.4" version-code="5"> <application application="weather/WeatherApp" label="Weather" icon="icon.png" /></manifest>They reach the PAPK as the version-code, label and icon manifest keys
(papk-info prints them). A PAPK whose manifest lacks them still installs: a
missing code reads as 1, a missing label as the package name, a missing icon as
none.
Screens and features
Two more elements, both optional, from app portability:
<?xml version="1.0" encoding="utf-8"?><manifest package="picoclock" version="1.0"> <application application="picoclock/ClockApp" label="Clock" /> <supports-screens design-width="320" design-height="480" /> <uses-feature name="picodroid.hardware.touchscreen" required="true" /></manifest><supports-screens> names the logical size the app was laid out against. The
runtime then shows the app in a window of exactly that size on every board —
Display.getWidth()/getHeight(), every dp, every coordinate the app uses and
every overlay the framework draws are the design’s. On a larger panel the window
is centred and the rest of the panel stays dark; on a smaller one the window
starts at the panel’s origin and the screen pans to what does not fit (a drag on
a touch board, the focus on a four-key one). The simulator logs the placement at
launch: window 320x240 @ (0,120) on the 320x480 panel, or
window: the 320x240 panel when nothing was declared. Declare a design size for
an app drawn in pixels against one panel; leave it out — the default — for an
app whose layout stretches with match_parent and layout_weight, which runs
at the panel’s own size (see
Writing a layout once for every board).
<uses-feature> declares a hardware feature by the name
PackageManager.hasSystemFeature answers to: picodroid.hardware.touchscreen,
picodroid.hardware.wifi, picodroid.hardware.ethernet. With
required="true" the installer — pdb install, the launcher’s installer and
the simulator’s apps install — refuses the app on a board that lacks the
feature, before anything is erased:
device rejected install: ERR — requires a feature this board lacks (uses-feature).
The default is required="false", which is informational: the stock widgets
work with a finger and with four keys alike, so only an app that reads raw
touch (a drawing pad, a drag demo) has to insist. A board whose firmware was
flashed with such an app directly, bypassing the installer, runs it and logs
app requires <feature>, which this board lacks once.
They reach the PAPK as the design-width, design-height and
requires-features (comma-separated, required ones only) manifest keys;
papk-pack takes them as --design-size WxH and --requires-feature <name>.
How it is wired
Each app’s build.gradle.kts applies a single plugin:
plugins { id("picodroid-papk")}The picodroid-papk plugin compiles the Java, optionally shrinks framework
references, and packs a build/papk/<name>.papk bundle (see
What the PAPK carries). It also:
- Adds the
:sdkproject as animplementationdependency, so every app compiles against the Picodroid SDK. - Roots the Java source set at the project directory and includes
**/*.java. That means both layouts work: sources nested underjava/<pkg>/(whatnewAppscaffolds) and.javafiles dropped flat in the project directory.
The PAPK package name is package=
The package-name key the PAPK carries is the manifest’s package= attribute,
and it is the identity everything on the device keys on: the package directory
holds one install per package, a reinstall replaces the previous copy, and
per-app storage lives under it. (Until September 2026 the key was the Gradle
project name instead; nothing consumed it then.)
The Gradle project name is a separate thing: the examples/<name>/
directory name, which picks the Gradle task names and the output file
build/papk/<name>.papk. Apps are auto-discovered by settings.gradle.kts:
any examples/<name>/ directory containing a file literally named
PicodroidManifest.xml becomes a subproject. No edit to settings.gradle.kts
is needed to add an app.
// Auto-discover every examples/<name>/ that ships a PicodroidManifest.xml.rootDir.resolve("examples").listFiles() ?.filter { it.isDirectory && it.resolve("PicodroidManifest.xml").isFile } ?.sortedBy { it.name } ?.forEach { include(":examples:${it.name}") }By convention the slash-form entry point’s leading segment, the package=
value, and the directory name all match (e.g. helloworld). Nothing enforces
that match — a reverse-DNS package= over a short directory name is fine —
but keeping them identical avoids confusion.
Source for the plugin and discovery: buildSrc/ and settings.gradle.kts.
What the PAPK carries
A .papk is a flat little-endian container: a 28-byte header, then a MANIFEST
section, a CLASSES section, and an ASSETS and a RESOURCES section when the app
has images or resources. Every section
starts on a 4-byte boundary, because the device reads all of them in place from
flash.
- MANIFEST holds the values of this page as key/value strings: the entry
point (
application,activityormain-class),package-name,version,framework-map-version, andversion-code,label,icon,design-width,design-heightandrequires-featureswhen set; an Android-form manifest addsactivitiesandservices(comma-joined, slash form) andtheme(the theme’sR.styleid, decimal). - CLASSES holds every
.classfile with a link tablepapk-packbuilt for it (the record a class loader would otherwise parse at run time, a hash of each superclass and interface name, a signature hash per method and a 4-byte descriptor per method reference) and a class index sorted by name hash. The runtime parses nothing when the app starts and finds classes and methods by hash. The framework’s own classes are embedded in the firmware in the same form.
This is format version 3; pdb install, papk-info and the firmware build read
no other, and a board does not load a file of another version left in its app
region. Packing also refuses a class file over 65,535 bytes and two classes
whose names hash alike.
./scripts/papk-info.sh build/apks/<name>.papk prints the header, the manifest
keys, the classes with their sizes, the assets and the resource counts, and
fails on a file that any of the above would refuse.
Scaffolding a new app
The fastest way to start is the newApp Gradle task:
./gradlew newApp -Pname=myappThe -Pname value must match ^[a-z][a-z0-9_]*$ — lowercase first letter, then
lowercase letters, digits, or underscores. A bad name fails with:
app name must match [a-z][a-z0-9_]* — got '<name>'Omitting -Pname fails with missing -Pname=<appname>, and an existing target
fails with already exists: <dir>.
It generates three files under examples/myapp/:
java/myapp/Myapp.java— anApplicationsubclass with anonCreate()stub.PicodroidManifest.xml—package="myapp",version="1.0", andapplication="myapp/Myapp".build.gradle.kts— appliesid("picodroid-papk").
The class name is the app name with its first letter uppercased. From there, follow Your first app.
Shrinking
Under --shrink the shrinker rewrites only framework
references — the picodroid.* and java.* classes and the member names the
release map covers. The release map is cut from the framework source tree
alone, so your app’s own class names pass through unchanged, and your
application= / activity= / main-class= value (e.g. myapp/MyApp) is
identical in the shrunk and unshrunk PAPK. Only the references baked into your
.class bytes (the picodroid/app/Application superclass, an onCreate
override) are rewritten, and the firmware’s copies are rewritten the same way.
Under --shrink --shrink-app your own classes are renamed too (myapp/MyApp
→ c/A) and papk-pack spells the manifest entry, the declared activities
and services, and a layout’s custom view class names through the per-app map,
so the manifest you write keeps saying myapp/MyApp while the packed manifest
names the class that is actually in the PAPK. The entry points Rust invokes by
name — main and the generated injectMembers — are kept by sdk/keep.toml.
You never need a keep entry for your own classes in either mode. See
App shrinking.
Runtime dispatch and errors
At load time the firmware first runs a compatibility check (below), loads the framework and app classes, then dispatches on the manifest entry point. Unlike the build-time “exactly one of” rule, the runtime uses a fixed precedence — it does not re-check that only one key is set:
application(highest precedence)activitymain-class
A normally-built PAPK only ever carries one of these, so precedence is moot. A hand-built PAPK with several keys would simply use the highest one.
If the named class is missing or its name is mistyped (for example, a dotted
myapp.MyApp that can’t match the slash-form internal name), the JVM’s
name-based lookup fails with MethodNotFound. The firmware logs the error and
returns cleanly rather than panicking — a bad APK should not brick the boot
path. The log prefix depends on the entry-point type:
application→Application.onCreate error: <err>activity→failed to instantiate initial Activity <class>, and lifecycle invocation errors asActivity lifecycle error: <err>main-class→ on hardwareJVM error: <err>; in the simulator[jvm] error: <err>
If you see one of these in the simulator or device log, the usual cause is a typo’d entry-point string or a dotted name. See Debugging and Troubleshooting.
Install compatibility check
Every PAPK embeds a framework-map-version key, written by the packer. The
firmware only loads a file that is format version 3 and whose link tables match
their class bytes (checked when the app region is scanned at boot); at load time
it compares the key against the version it was built with (verify_compat). The two sides share one rule: both unshrunk (0.0.0) is OK,
both shrunk with a compatible map is OK, and an asymmetric --shrink setting (or
a PAPK built newer than the firmware) is rejected.
The same check runs host-side during pdb install, so an incompatible PAPK is
refused before flashing:
Refusing to install: PAPK is incompatible with running firmware. ... Rebuild the PAPK with matching --shrink setting (see reference/shrinker in the docs).The fix is almost always to rebuild the PAPK with the same --shrink setting as
the firmware. See Hot-swap installs,
the shrinker reference, and
Troubleshooting.