Skip to content

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 the package-name key 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, default 1). 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 optional android:theme and 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’s assets/ 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-pack warns 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 its PackageManager.FEATURE_* name; required defaults to false. 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>' (and design-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 (the verifyManifest task)
  • manifest <service android:name="<class>"> is not among the packed classes and … 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 the MAIN intent 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 manifest package with a leading dot. The short-form activity= 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-pack checks that each class is packed and extends picodroid.app.Activity / Service, and the Gradle verifyManifest task checks that every new 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.setClassName is dynamic and is not checked.
  • android:theme="@style/Name" on <application>: the <style> in res/values that ?attr/… resolves against at build time and that the framework applies before the first Activity’s onCreate, as Android does, so the app does not call setTheme. Without it, the style called AppTheme serves ?attr/ and nothing is applied (styles and the theme). One theme per app: an android:theme on 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 a BroadcastReceiver the framework starts: for the actions its <intent-filter> names — BOOT_COMPLETED, TIME_SET and TIMEZONE_CHANGED are 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 or PendingIntent.getBroadcast that 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, then onReceive with 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 extend picodroid.content.BroadcastReceiver.
  • Not read: <meta-data>, <provider>, anything in an <intent-filter> but the MAIN action and a receiver’s actions. An unknown child of <application> fails the build naming what is supported. The bare spellings name= and theme= are accepted beside the android: 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 :sdk project as an implementation dependency, 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 under java/<pkg>/ (what newApp scaffolds) and .java files 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, activity or main-class), package-name, version, framework-map-version, and version-code, label, icon, design-width, design-height and requires-features when set; an Android-form manifest adds activities and services (comma-joined, slash form) and theme (the theme’s R.style id, decimal).
  • CLASSES holds every .class file with a link table papk-pack built 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:

Terminal window
./gradlew newApp -Pname=myapp

The -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 — an Application subclass with an onCreate() stub.
  • PicodroidManifest.xml — package="myapp", version="1.0", and application="myapp/Myapp".
  • build.gradle.kts — applies id("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:

  1. application (highest precedence)
  2. activity
  3. main-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 as Activity lifecycle error: <err>
  • main-class → on hardware JVM 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.