This is the canonical in-repo reference for authoring a Code on the Go
plugin. It covers project layout, the AndroidManifest.xml meta-data
contract, theme-aware icons, building, installing, and common failure
modes.
For higher-level architecture and product-side context, see the Plugin Development wiki page.
The closest in-tree example today is apk-viewer-plugin/. Note that
the existing sample plugins do not yet ship theme-aware icons — the
icon section below is the source of truth until they're updated.
A plugin is a standalone Android module that compiles to a .cgp file
(a renamed APK). Recommended layout:
my-plugin/
├── build.gradle.kts
├── proguard-rules.pro
├── settings.gradle.kts
└── src/main/
├── AndroidManifest.xml # All plugin metadata lives here
├── assets/ # Theme icons go here (PNG/WebP)
│ ├── icon_day.png
│ └── icon_night.png
├── kotlin/.../MyPlugin.kt # Implements IPlugin
└── res/ # Standard Android resources (layouts, etc.)
├── layout/
└── values/
The build is wired up by applying com.itsaky.androidide.plugins.build
in build.gradle.kts:
plugins {
id("com.android.application") version "8.8.2"
id("org.jetbrains.kotlin.android") version "2.1.21"
id("com.itsaky.androidide.plugins.build")
}
pluginBuilder {
pluginName = "my-plugin" // becomes my-plugin.cgp
pluginVersion = "1.0.0"
}The IDE-side API your plugin compiles against ships as a Maven coordinate,
injected into the on-device local Maven repository during onboarding — so it
resolves offline, with no libs/*.jar to commit:
dependencies {
compileOnly("com.itsaky.androidide:plugin-api:1.0.0")
}plugin-api:1.0.0 is a single jar bundling the API surface plugins compile
against — the :plugin-api module plus common, eventbus-events, and
idetooltips. The builder plugin applied above resolves the same way, from the
injected com.itsaky.androidide.plugins.build 1.0.0 marker.
The plugins {} example above uses AGP 8.8.2 / Kotlin 2.1.21 — the versions the
dev/CI repo resolves online. A plugin built on-device resolves AGP and the Kotlin
Gradle plugin from the harvested on-device localMvnRepository, which currently ships
only AGP 8.11.0 and Kotlin 1.9.22. Request those versions for an on-device
build, or offline resolution of the build plugins fails.
Also declare AGP apply false in the root build.gradle.kts (as the standard CoGo
project template does):
plugins {
id("com.android.application") apply false version "8.11.0"
id("com.android.library") apply false version "8.11.0"
}CoGo injects LogSenderPlugin (via its build init script) into every module; that
plugin references AGP's ApplicationVariant and is loaded from the root buildscript
classpath, so a root without AGP fails configuration with
NoClassDefFoundError: com/android/build/api/variant/ApplicationVariant.
The in-app plugin-project detector still recognizes a project by a
libs/plugin-api.jaron disk; dropping that on-disk requirement so a coordinate-only plugin is recognized is tracked in ADFA-4913.
All plugin metadata is declared as <meta-data> tags inside
<application> in src/main/AndroidManifest.xml. The loader reads
these in PluginLoader.getPluginMetadata()
(plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt:227).
| Key | Type | Required | Notes |
|---|---|---|---|
plugin.id |
string | yes* | Unique plugin identifier. Falls back to the APK's package name. |
plugin.name |
string | yes* | Display name. |
plugin.version |
string | yes* | Defaults to "1.0.0". Prefer ${pluginVersion} from pluginBuilder. |
plugin.description |
string | no | Shown in the plugin list. |
plugin.author |
string | no | |
plugin.main_class |
string (FQCN) | yes | FQCN of your IPlugin implementation. Plugin fails to load if absent. |
plugin.min_ide_version |
string | yes* | Defaults to "1.0.0". |
plugin.max_ide_version |
string | no | |
plugin.permissions |
string (comma-separated) | no | See Permissions. |
plugin.dependencies |
string (comma-separated) | no | Plugin IDs this plugin requires. |
plugin.sidebar_items |
int | no | Number of sidebar entries this plugin contributes. Default 0. |
plugin.icon_day |
string (ZIP path) | see below | Asset path inside the .cgp for the light-theme icon. |
plugin.icon_night |
string (ZIP path) | see below | Asset path inside the .cgp for the dark-theme icon. |
* The loader supplies a default if the tag is absent, so the install won't fail, but the plugin will be hard to identify in the UI.
To choose plugin.min_ide_version, see the
Plugin API Changelog — it maps each plugin
capability to the YY.WW release that first shipped it.
Example (adapted from apk-viewer-plugin/src/main/AndroidManifest.xml):
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="My Plugin"
android:theme="@style/PluginTheme">
<meta-data android:name="plugin.id" android:value="com.example.myplugin" />
<meta-data android:name="plugin.name" android:value="My Plugin" />
<meta-data android:name="plugin.version" android:value="${pluginVersion}" />
<meta-data android:name="plugin.description" android:value="One-line description." />
<meta-data android:name="plugin.author" android:value="Your Name" />
<meta-data android:name="plugin.main_class" android:value="com.example.myplugin.MyPlugin" />
<meta-data android:name="plugin.min_ide_version" android:value="1.0.0" />
<meta-data android:name="plugin.permissions" android:value="filesystem.read" />
<meta-data android:name="plugin.sidebar_items" android:value="1" />
<meta-data android:name="plugin.icon_day" android:value="assets/icon_day.png" />
<meta-data android:name="plugin.icon_night" android:value="assets/icon_night.png" />
</application>
</manifest>extensions and build_actions are not readable from
AndroidManifest.xml meta-data — they're only honored when supplied
via a plugin.json fallback manifest. If your plugin needs either,
use the JSON form (see PluginManifest.kt).
The plugin manager renders a different icon based on whether the system
is in light or dark mode (PluginListItem.kt:69-70). To opt in, ship
two raster icons in your plugin and point at them from the manifest.
Put your icons in src/main/assets/:
src/main/assets/
├── icon_day.png
└── icon_night.png
Do not use res/drawable/ or res/raw/. The loader does a
literal ZIP-entry lookup against the compiled .cgp
(PluginLoader.kt:207), and AAPT2 rewrites resource paths during the
build (vector XML becomes compiled binary XML; bitmaps may move to
density-qualified directories). Only assets/<name> is preserved
verbatim, so it's the only location where the path you write in the
manifest matches the path the loader will find.
- PNG (recommended)
- WebP
- JPEG
Not supported: raw SVG, Android vector drawable XML (compiled or
not). Icons are decoded with BitmapFactory (FileImage.kt:102), which
handles raster formats only. Convert SVG sources to PNG yourself
before bundling.
96×96 px square. The plugin list renders icons at roughly 48dp, so 96 px covers xxxhdpi devices with headroom.
<meta-data android:name="plugin.icon_day"
android:value="assets/icon_day.png" />
<meta-data android:name="plugin.icon_night"
android:value="assets/icon_night.png" />The value is the literal path inside the .cgp ZIP, not an
Android resource reference. No @drawable/..., no leading slash.
If your plugin is built with assemblePluginDebug (i.e. the resulting
APK has FLAG_DEBUGGABLE), the installer rejects the plugin
unless both plugin.icon_day and plugin.icon_night are declared
and both files are present inside the .cgp. The check is in
PluginRepositoryImpl.kt:105, and the error message is verbatim:
[<pluginId>] Missing <keys> for debug plugin. Debug plugins must declare and ship both icon_day and icon_night assets.
The uploaded .cgp is deleted when this fires. Release builds may
omit icons; they fall back to the generic ic_extension puzzle-piece
drawable, tinted with ?attr/colorOnSurface so it adapts to the
theme automatically.
On install, both icons are extracted to a per-plugin directory on the device:
/data/data/<app-id>/app_plugin_icons/<plugin-id>/icon_day.<ext>
/data/data/<app-id>/app_plugin_icons/<plugin-id>/icon_night.<ext>
Useful for confirming the install actually unpacked the files (see Troubleshooting below).
From the repo root, with the flox environment active:
flox activate -d flox/local -- ./gradlew :my-plugin:assemblePlugin # release
flox activate -d flox/local -- ./gradlew :my-plugin:assemblePluginDebug # debugOutput:
- Release:
my-plugin/build/plugin/my-plugin.cgp - Debug:
my-plugin/build/plugin/my-plugin-debug.cgp
To install, transfer the .cgp to the device and use the in-app
plugin manager (Settings → Plugins → Install). The installer
validates the manifest, extracts icons, and registers the plugin.
Declare permissions as a comma-separated list in plugin.permissions.
Defined in plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/IPlugin.kt:28:
| Key | Grants |
|---|---|
filesystem.read |
Read files from the project directory |
filesystem.write |
Write files to the project directory |
network.access |
Access network resources |
system.commands |
Execute system commands |
ide.settings |
Modify IDE settings |
project.structure |
Modify project structure |
native.code |
Execute native machine code |
ide.environment.write |
Write to IDE-managed directories (Android SDK, NDK, cache) |
Install fails with "Missing icon_day and icon_night for debug plugin"
Your debug build is missing one or both icons. Check:
- Both
<meta-data>tags are present inAndroidManifest.xml. - Both files exist in
src/main/assets/. - After building,
unzip -l my-plugin/build/plugin/my-plugin-debug.cgp | grep assetsshows bothassets/icon_day.pngandassets/icon_night.png.
Plugin lists shows the generic puzzle-piece icon instead of yours
The loader couldn't find the file at the declared ZIP path. The
manifest value must match the actual path inside the .cgp byte-for-byte.
# Inspect what the manifest declares:
aapt2 dump xmltree my-plugin/build/plugin/my-plugin.cgp --file AndroidManifest.xml | grep icon_
# Inspect what's actually in the ZIP:
unzip -l my-plugin/build/plugin/my-plugin.cgp | grep -E 'assets|icon'Common causes: typo in the manifest path, file accidentally placed
under res/raw/ or res/drawable/, or a leading slash on the
manifest value (use assets/icon_day.png, not /assets/icon_day.png).
Wrong icon shows for the current theme
The selection happens in PluginListItem.kt:69-70 via
isSystemInDarkMode(). Verify your device is actually in the theme
you expect (system Settings → Display). Also verify both files
extracted to the device:
adb shell run-as <your.app.id> ls app_plugin_icons/<plugin-id>/