extensionDeveloper documentation

Build something powerful for Dolphy.

A plugin is one readable Python file with the .dolphyplugin extension. It can create screens, integrate hardware and extend existing Dolphy features.

deployed_code
01
auto_awesome

What a plugin can do

Plugins run inside Dolphy and use the same design system and hardware access layer as the app.

dashboard_customizeCreate complete screens

Compose multi-screen Material 3 Expressive tools with state and callbacks.

hubExtend Dolphy

Add overlays, FABs, replace screens and hook named actions.

usbIntegrate hardware

Work with Bluetooth, Wi-Fi, NFC, USB, IR and Android services.

terminalUse system access

Call Java APIs and use Shell, Root or Shizuku when available.

warning
Trusted-code model

Plugins are not sandboxed. Read the source and only install code you trust.

02
badge

Plugin metadata

Put metadata at the beginning of the text file. Dolphy reads it before installation without executing Python.

my_plugin.dolphyplugin
__id__ = "my_wireless_lab"
__name__ = "My Wireless Lab"
__version__ = "1.0.0"
__author__ = "Developer"
__description__ = "Wireless diagnostics"
__icon__ = "rounded:radar"
__library__ = False
__design_library__ = False
__dependencies__ = ["shared_radio_core"]
__permissions__ = ["ble_spam", "download", "files"]
__id__Stable lowercase identifier
__name__Name shown in Dolphy
__version__Plugin version
__author__Developer name
__description__Short description
__icon__Material Icon: filled, outlined, rounded, sharp or twotone
__library__Load before regular plugins as a shared library
__design_library__Mark a shared design library
__dependencies__Required plugin ids loaded before this plugin
__permissions__Capabilities shown before installation; Dolphy also detects direct API usage in the source
Manifest parsing rules

The file is UTF-8 text and may be at most 48 MiB. String fields must be top-level single- or double-quoted assignments. id is lowercased, invalid characters become underscores and the result is limited to 64 characters. Missing fields fall back to the filename, version 1.0 and icon extension.

.dolphypluginapplication/x-dolphy-pluginUTF-848 MiB max[a-z0-9_-], 64 chars
library_books
Library plugins and dependencies

Library plugins start before consumers. __dependencies__ declares required plugin ids, determines startup order and prevents a consumer from starting while a dependency is missing or disabled. Java classes and objects may be exported by name and imported by dependent plugins.

code
Public format and runtime

A file opened or shared as .dolphyplugin is always executed by the embedded Python runtime. The repository still contains an internal JavaScript compatibility runtime for previously stored .js plugins, but it is not the documented public .dolphyplugin authoring format.

03
rocket_launch

Minimal working plugin

Extend BasePlugin, register a module during load and return a UI tree from screen_main.

hello_dolphy.dolphyplugin
__id__ = "hello_dolphy"
__name__ = "Hello Dolphy"
__version__ = "1.0.0"
__icon__ = "rounded:waving_hand"

class HelloDolphy(BasePlugin):
    def on_plugin_load(self):
        self.add_module(
            title="Hello Dolphy",
            description="My first plugin",
            icon="waving_hand",
            screen="main",
            section="PLUGINS"
        )

    def screen_main(self, ui, api, state):
        return ui.scaffold(
            title="Hello Dolphy",
            content=ui.column([
                ui.card([
                    ui.text("It works!", "headlineSmall"),
                    ui.button("Say hello", self.say_hello)
                ])
            ], padding=16, spacing=12)
        )

    def say_hello(self):
        self.toast("Hello from Python")
Complete BasePlugin API

These helpers delegate to the native bridge. api and ui are also available directly on self.

add_module(title, description="", icon="extension", screen="main", section="PLUGINS", order=0)navigate(screen)navigate_app(route)refresh()get_setting(key, default=None)set_setting(key, value)toast(text)hook_screen(route, screen="main", mode="overlay", priority=0)hook_surface(surface, screen="main", mode="overlay", priority=0)provide_service(service, priority=0)hook_action(action, priority=0)load_dex_base64(module, encoded, sha256="")load_dex_file(module, path, sha256="")dex_class(module, class_name)new_dex_instance(module, class_name)export_dex_class(name, module, class_name)export_app_class(name, class_name)export_java_class(name, java_class)import_java_class(name)export_java_object(name, value)import_java_object(name)java_exports()root(command)shizuku(command)shell(command)
view_module
Where add_module can place a card

Built-in sections are INFRARED, BLUETOOTH, OTHER and PLUGINS. Aliases such as ir, bt, ble, misc and localized names are normalized; any other text creates a custom section. order sorts cards, screen chooses the plugin screen, and icon accepts Material icon syntax.

04
palette

Material 3 Expressive UI

Python describes a UI tree. Dolphy renders native Jetpack Compose components with the current theme and accent color.

codePython ui.*
arrow_forward
account_treeUI tree
arrow_forward
androidCompose MD3
ui.scaffoldui.columnui.lazy_columnui.cardui.segmented_listui.function_rowui.buttonui.split_buttonui.icon_buttonui.text_fieldui.switchui.sliderui.tabsui.dropdownui.floating_toolbarui.wavy_progress
text.styleheadlineLarge/Medium/Small, titleLarge/Medium/Small, bodyLarge/Medium/Small and labelLarge/Medium/Small; camelCase and snake_case are accepted.
color / tintprimary/accent, muted/secondary, error/red, success/green, warning/orange, or #RGB/#ARGB/#RRGGBB/#AARRGGBB.
iconA built-in alias or any bundled Material icon as style:name: filled, outlined, rounded, sharp, twotone or automirrored.
image.sourceasset path, asset:// URL, data:image/...;base64 URI or raw Base64. Scale: fit, crop, fill, fillWidth, fillHeight, inside or none.
callbacksClick receives no value; text fields receive str, switches and checkboxes bool, sliders float, tabs int, and option controls the selected value.
WebViewLoads either url or html, with JavaScript and DOM storage enabled. Treat remote and embedded content as trusted executable UI.
Complete UiFactory signatures

Every argument below is supported by the Python runtime. Dimensions and spacing use dp.

scaffold(content, title=None, show_back=True, fab=None, actions=None)top_action(icon, on_click=None)column(children=None, padding=0, spacing=8, fill_max_size=False)row(children=None, padding=0, spacing=8, fill_max_width=True)box(children=None, padding=0, fill_max_size=False)lazy_column(children=None, padding=0, spacing=4, fill_max_size=True)lazy_row(children=None, padding=0, spacing=8, fill_max_width=True)text(text, style="bodyMedium", color=None, max_lines=2147483647)button(text, on_click=None, style="filled", enabled=True, fill_max_width=False)bounce_button(text, on_click=None, enabled=True, fill_max_width=True)split_button(text, on_click=None, on_secondary=None, enabled=True, fill_max_width=True)icon_button(icon, on_click=None, enabled=True)text_field(value="", on_change=None, label="", single_line=True, fill_max_width=True)switch(checked=False, on_change=None, title="", subtitle="", enabled=True)checkbox(checked=False, on_change=None, title="", subtitle="", enabled=True)slider(value=0, on_change=None, minimum=0, maximum=100, title="", steps=0)linear_progress(progress=None, fill_max_width=True)circular_progress(progress=None)wavy_progress(progress=None, size=64)divider(vertical=False)spacer(height=8, width=0)icon(name, size=24, tint=None)image(source, width=None, height=None, scale="fit", corner_radius=0, fill_max_width=False, content_description="")chip(text, selected=False, on_click=None)card(children=None, padding=16, index=None, count=None, on_click=None)function_row(title, description="", icon="extension", icon_tint=None, on_click=None)segmented_list(children=None, spacing=4)settings_row(title, subtitle="", icon=None, on_click=None, trailing=None)tabs(tabs, selected=0, on_select=None)button_group(options, selected, on_select=None)floating_toolbar(items)toolbar_item(icon, on_click=None, selected=False, label="")radio_group(options, selected, on_select=None, title="")dropdown(options, selected, on_select=None, label="", enabled=True)webview(url="", html="", fill_max_size=True, height=320)log_panel(text, max_height=200)alert_dialog(show, title, message, confirm_text="OK", dismiss_text="Cancel", on_confirm=None, on_dismiss=None, cancelable=True)empty()
Native plugin settings

api.registerSettings adds Material 3 Expressive sections directly to Dolphy Settings. Switches and sliders persist in the plugin store and emit setting_changed; nav and card items open plugin screens.

headerswitchslidernavcard
plugin_settings.py
import json

def on_plugin_load(self):
    self.api.registerSettings("Radio toolkit", json.dumps([
        {"type": "switch", "key": "capture", "title": "Capture packets", "subtitle": "Keep a local trace", "default": True},
        {"type": "slider", "key": "power", "title": "Power", "min": 1, "max": 100, "default": 40, "steps": 98},
        {"type": "nav", "title": "Advanced", "icon": "tune", "screen": "advanced"}
    ]), 20)

def on_setting_changed(self, payload):
    key = payload["key"]
    value = payload["value"]
headertitle
switchkey, title, subtitle, default: bool
sliderkey, title, subtitle, min, max, default, steps
navtitle, subtitle, icon, screen; always opens plugin/{pluginId}/{screen}
cardtitle, subtitle, icon and optional screen
save
Storage and updates

Switch and slider values use the same per-plugin JSON store as get_setting/set_setting. The host persists a value before calling on_setting_changed({key, value}); sliders emit the event when interaction finishes.

Where setting keys come from

There is no predefined key catalog. The plugin author creates any string key and uses the same spelling when reading, writing or declaring a Settings item. Keys only collide inside the same plugin because every pluginId has a separate store. Prefer stable names such as capture_enabled or radio.power; changing a key creates a new setting.

set_setting("capture_enabled", True)get_setting("capture_enabled", False){"type":"switch", "key":"capture_enabled", ...}
keyAny non-empty string chosen by the plugin. There are currently no reserved host keys.
valueAny json.dumps-compatible value: None, bool, number, str, list or dict.
defaultget_setting returns the supplied default when the key has never been saved.
lifetimeValues survive restarts and enable/disable. Deleting the plugin removes its entire settings store.
05
conversion_path

Integrate with existing features

Plugins can participate in Dolphy instead of living on an isolated screen.

fact_check
Integration points, not arbitrary patching

Screen hooks work for every route hosted by Dolphy. Action and service hooks work only where the app explicitly dispatches a named integration point. A plugin does not automatically intercept every Kotlin function.

layers
hook_screen(route, screen, mode, priority)

Add an overlay, top or bottom panel, FAB, or replace a route. Wildcards such as ir_* are supported.

bolt
hook_action(action, priority)

Inspect or change data, mark an action handled, or cancel it.

settings_input_component
provide_service(service, priority)

Provide an alternative hardware or logic backend with automatic fallback.

Screen hooks in detail

Routes and patterns are case-insensitive. The asterisk matches any number of characters. If several plugins match, higher priority wins where only one contribution can be used.

overlayRenders a full-size transparent layer over the existing screen.
topPlaces plugin UI in a Material surface at the top of the screen.
bottomPlaces plugin UI above the bottom navigation area.
fabPlaces compact plugin UI at the bottom end with navigation-safe padding.
replaceThe highest-priority replacement visually covers the built-in screen. Its underlying Compose screen still exists, so this is UI replacement rather than bytecode patching.
Useful route patterns
ir_*other/ir_*nfc_*other/nfc_*lan_*other/lan_*ble_section/*plugin/**

Use the navigation event while developing to inspect the current route. Routes with arguments are reported as their navigation template, for example nfc_result/{id}.

Replace complete core screens

Use replace with other, bluetooth or settings to cover the complete core screen. Bluetooth also exposes three live sub-screen targets, so a plugin can replace All, BLE or Advert independently.

otherbluetoothsettingsbluetooth/allbluetooth/blebluetooth/advert
App chrome surfaces

hook_surface targets UI outside an ordinary screen route. bottom_bar can be overlaid or fully replaced. The replacement follows plugin priority and disappears automatically when the plugin is disabled.

hook_surface("bottom_bar", "panel", "replace", 100)surface/bottom_bar
bluetooth_redesign.py
def on_plugin_load(self):
    self.hook_screen("other", "my_other", "replace", 100)
    self.hook_screen("settings", "my_settings", "replace", 100)
    self.hook_screen("bluetooth/all", "my_all", "replace", 100)
    self.hook_screen("bluetooth/ble", "my_ble", "replace", 100)
    self.hook_screen("bluetooth/advert", "my_advert", "replace", 100)
bottom_bar_extension.py
def on_plugin_load(self):
    self.hook_surface("bottom_bar", "custom_bar", "replace", 100)

def screen_custom_bar(self, ui, api, state):
    return ui.floating_toolbar([
        ui.toolbar_item("bluetooth", lambda: self.navigate_app("bluetooth"), label="Bluetooth"),
        ui.toolbar_item("extension", lambda: self.navigate_app("other"), label="Functions"),
        ui.toolbar_item("settings", lambda: self.navigate_app("settings"), label="Settings")
    ])
screen_hooks.py
def on_plugin_load(self):
    self.hook_screen("ir_*", "ir_badge", "fab", 50)
    self.hook_screen("nfc_tools", "nfc_panel", "bottom", 20)
    self.hook_screen("qr_tools", "replacement", "replace", 100)

def screen_ir_badge(self, ui, api, state):
    return ui.icon_button("usb", lambda: self.navigate("hardware"))

def screen_hardware(self, ui, api, state):
    return ui.scaffold(
        title="External hardware",
        content=ui.text("Connected")
    )

def screen_nfc_panel(self, ui, api, state):
    return ui.card([
        ui.text("Plugin panel on the existing NFC screen")
    ])

def screen_replacement(self, ui, api, state):
    return ui.scaffold(
        title="My QR tools",
        content=ui.column([
            ui.text("This plugin replaced the built-in QR screen", "headlineSmall"),
            ui.button("Open original section", lambda: self.navigate_app("other"))
        ], padding=16, spacing=12, fill_max_size=True)
    )

Action hooks and services

Action hooks form a priority-ordered pipeline. Services form a priority-ordered provider list. Both use JSON-compatible payloads.

NoneContinue without changing the action.
{"payload": {...}}Replace the payload passed to lower-priority hooks and the built-in implementation.
{"handled": true}Mark the action handled by the plugin. Use result to return a value.
{"cancel": true}Stop the hook chain and cancel the built-in operation.
FalseCancel immediately. True marks the action handled.
account_tree
Integration points available now

Dolphy exposes shared infrared transmission plus Bluetooth section selection, All actions, BLE modes, Advert presets and every outgoing BLE advertising packet. The complete exact list is in the hook catalog.

Add a first-class BLE Spam mode

register_ble_mode adds the plugin beside the built-in BLE modes using the same card, active state, shared delay and Stop All behavior. Dolphy sends ble_mode_start, ble_mode_stop and ble_mode_delay_changed only to the owning plugin. The plugin owns its advertising loop and must stop it on both stop and unload.

custom_ble_mode.dolphyplugin
def on_plugin_load(self):
    self.register_ble_mode("lab_beacon", "Lab Beacon", "Custom advertising payload", "radar")

def on_ble_mode_start(self, payload):
    if payload["modeId"] == "lab_beacon":
        self.start_loop(payload["delayMs"])

def on_ble_mode_delay_changed(self, payload):
    self.delay_ms = payload["delayMs"]

def on_ble_mode_stop(self, payload):
    self.stop_loop()

def on_plugin_unload(self):
    self.stop_loop()

Download and apply plugin assets

download_assets requests all files as one batch. Each destination is confined to this plugin’s private directory; maxBytes limits each response and sha256 can verify it. Unless the user disabled confirmation in Plugin settings → Security, Dolphy shows the plugin name, file count and declared total size before network transfer. apply_asset imports a saved file into a stock Bad USB editor, user IR remotes or QR Audio Spoofer custom HTML.

asset_pack.dolphyplugin
def fetch_remote(self):
    files = [{
        "url": "https://example.org/remotes/tv.ir",
        "path": "remotes/tv.ir",
        "sizeBytes": 24576,
        "maxBytes": 100000,
        "sha256": "published SHA-256"
    }]
    self.download_assets(files, self.downloaded)

def downloaded(self, result_json):
    result = json.loads(str(result_json))
    if result.get("ok"):
        self.apply_asset("ir_remote", "remotes/tv.ir", "My TV")
asset_path(path)Returns the absolute path of a location inside the plugin’s private folder for Java and DEX APIs.
download_assets(items, callback)Items accept url, path, sizeBytes, maxBytes, sha256 and headers. The callback receives {ok, files} as JSON text.
apply_asset(type, path, title="")Supported types: bad_hid, ir_remote and qr_audio_html. Returns a parsed result dictionary.

Example: replace BLE advertising logic

bluetooth.advertising.transmit runs for custom Advert presets and packets generated by every built-in BLE spam mode. Change data to rewrite the packet, return cancel to block it, or handled after transmitting with your own backend.

ble_backend.dolphyplugin
def on_plugin_load(self):
    self.hook_action("bluetooth.advertising.transmit", 100)
    self.hook_action("bluetooth.advert.preset.*", 80)

def hook_bluetooth_advertising_transmit(self, payload):
    source = payload["metadata"]["source"]
    packet = payload["data"]
    if self.external_radio_ready():
        self.send_external(packet, payload.get("scanResponse"))
        return {"handled": True}
    packet["includeDeviceName"] = False
    return {"payload": payload}
integration.py
def on_plugin_load(self):
    self.hook_screen("ir_*", "usb_status", "fab", 50)
    self.hook_action("infrared.transmit", 50)
    self.provide_service("infrared.transmitter", 100)

def hook_infrared_transmit(self, payload):
    payload["frequency"] = self.normalize_frequency(payload["frequency"])
    return {"payload": payload}

def service_infrared_transmitter_available(self, payload):
    return {"available": self.usb_connected()}

def service_infrared_transmitter_transmit(self, payload):
    if not self.usb_connected():
        return {"handled": False}
    ok = self.send_usb(payload["frequency"], payload["pattern"])
    return {"handled": True, "ok": ok}

Example: intercept every IR Storm transmission

IR Storm calls the shared infrared.transmit action once for every signal. The action payload does not contain the source screen, so a plugin that targets only IR Storm must remember the latest navigation event.

ir_storm_hook.dolphyplugin
__id__ = "ir_storm_hook"
__name__ = "IR Storm Hook"
__version__ = "1.0.0"
__icon__ = "rounded:settings_input_antenna"

from java import jarray, jint

class IrStormHook(BasePlugin):
    def __init__(self):
        super().__init__()
        self.current_route = ""

    def on_plugin_load(self):
        self.hook_action("infrared.transmit", 100)

    def on_navigation(self, route):
        self.current_route = str(route)

    def hook_infrared_transmit(self, payload):
        if self.current_route not in ("ir_storm", "other/ir_storm"):
            return None

        frequency = int(payload["frequency"])
        pattern = [int(value) for value in payload["pattern"]]
        ok = self.send_to_external_ir(frequency, pattern)
        return {
            "handled": True,
            "result": ok
        }

    def send_to_external_ir(self, frequency, pattern):
        android = self.api.getAndroid()
        java_pattern = jarray(jint)(pattern)
        return android.irTransmit(frequency, java_pattern)
usb
Built-in and external transmitters

The example sends through Android's built-in ConsumerIrManager without re-entering the hook. For a USB transmitter, replace only send_to_external_ir with the device protocol implemented through api.getAndroid(), Android USB classes or a service provider.

nameSignal or button name when available
frequencyCarrier frequency in hertz
patternAlternating mark and space durations in microseconds
protocolProtocol label when known
codeOriginal encoded value when available
timingsOriginal timing representation when available
route
What this example affects

It intercepts both ir_storm and other/ir_storm. Returning None on every other route preserves ordinary remotes, TV Be Gone and IR Jammer. Remove the route check to intercept every Dolphy IR transmission.

{"payload": payload}Change frequency or pattern, then let Dolphy continue sending.
{"handled": true, "result": true}The plugin sent the signal; skip the built-in transmitter and report success.
{"cancel": true}Block the signal without sending it.
05A
deployed_code

DEX loading and library plugins

Keep a plugin as one readable Python file while embedding compiled Java or Kotlin code and sharing native capabilities with other plugins.

verified_user
DEX is native trusted code

A loaded DEX runs inside the Dolphy process with the app permissions. Always publish the SHA-256, verify it before loading and never execute an untrusted downloaded payload.

Embed and load a raw DEX

Store the raw DEX as Base64 in the .dolphyplugin file. Dolphy removes whitespace, decodes it, verifies SHA-256 and creates the class loader. Raw DEX uses InMemoryDexClassLoader on Android 8+; older Android and ZIP-based JAR/APK payloads use a private read-only code-cache file and DexClassLoader.

native_radio_core.dolphyplugin
__id__ = "shared_radio_core"
__name__ = "Shared Radio Core"
__version__ = "1.0.0"
__library__ = True

DEX_SHA256 = "48c3cb64ad3c0e82b7352549f7568e4e42119fc6b066a050054474e6fffca28c"
DEX_BASE64 = "ZGV4CjAzNQ..."

class SharedRadioCore(BasePlugin):
    def on_plugin_load(self):
        loader = self.load_dex_base64("radio_core", DEX_BASE64, DEX_SHA256)
        bridge_class = loader.loadClass("dev.dolphy.radio.RadioBridge")
        constructor = bridge_class.getDeclaredConstructor()
        constructor.setAccessible(True)
        bridge = constructor.newInstance()
        self.export_java_class("radio.bridge.class", bridge_class)
        self.export_java_object("radio.bridge", bridge)

    def on_plugin_unload(self):
        bridge = self.import_java_object("radio.bridge")
        if bridge is not None:
            bridge.close()
load_dex_base64(module, encoded, sha256="")Loads raw DEX, JAR or APK bytes stored as Base64 and returns the module ClassLoader.
load_dex_file(module, path, sha256="")Copies and loads a DEX/JAR/APK file. Use plugin-private storage and verify its digest.
dex_class(module, class_name)Loads a Class from a module owned by the current plugin.
new_dex_instance(module, class_name)Calls an accessible no-argument constructor and returns the Java object.
api.getDexClassLoader(module)Returns an already loaded module ClassLoader or None.
api.getDependencyClassLoader()Returns an app parent loader which can also resolve modules exported by declared dependencies.

Export classes and live objects

Exports are process-local named references. A library owns every class and object it exports; disabling, updating or deleting that library removes its modules and exports from the registry.

radio_consumer.dolphyplugin
__id__ = "radio_consumer"
__name__ = "Radio Consumer"
__dependencies__ = ["shared_radio_core"]

class RadioConsumer(BasePlugin):
    def on_plugin_load(self):
        self.bridge = self.import_java_object("radio.bridge")
        bridge_class = self.import_java_class("radio.bridge.class")
        if self.bridge is None or bridge_class is None:
            raise RuntimeError("Shared Radio Core did not export its API")
        self.add_module("Radio Console", "Uses the shared native bridge", "memory")

    def scan(self):
        result = self.bridge.scan()
        self.toast(str(result))
export_dex_class(name, module, class_name)Resolve and publish a class from the current DEX module.
export_app_class(name, class_name)Publish a Dolphy or dependency class by its fully qualified name.
export_java_class(name, java_class)Publish a Class object already obtained through Java reflection.
export_java_object(name, value)Publish a live Java object, adapter, controller or facade.
import_java_class(name)Get a named Class export from any enabled dependency.
import_java_object(name)Get a named live object export from any enabled dependency.
java_exports()Inspect all loaded modules and exported class/object names for diagnostics.

Expose Dolphy internals from open source

Use the repository to find a fully qualified JVM class name, then export that Class or wrap it in a stable DEX facade. Consumers should depend on the library facade instead of duplicating fragile reflection code.

dolphy_internal_facade.py
def on_plugin_load(self):
    self.export_app_class(
        "dolphy.main_activity",
        "com.droid.dolphy.MainActivity"
    )
    activity_class = self.import_java_class("dolphy.main_activity")
    methods = [method.getName() for method in activity_class.getDeclaredMethods()]

Java reflection helpers

These helpers search the app plus DEX modules of declared dependencies. They can inspect private members, read or write fields, call a matching method and construct an object. They do not install a method hook and do not intercept future calls.

find_java_class(class_name)Returns a Class or None using the app and dependency ClassLoaders.
inspect_java_class(java_class)Returns className plus inherited fields, methods, constructors, parameter types and static flags.
get_java_field(target, field_name)Reads a public or private instance field. Pass a Class for a static field.
set_java_field(target, field_name, value)Writes a public or private instance field. Pass a Class for a static field.
invoke_java(target, method_name, *args)Calls the first compatible overload by name, argument count and runtime types. Pass a Class for a static method.
new_java_instance(java_class, *args)Calls the first compatible declared constructor, including a private constructor.
reflection_library.dolphyplugin
__id__ = "dolphy_reflection_facade"
__name__ = "Dolphy Reflection Facade"
__library__ = True

class ReflectionFacade(BasePlugin):
    def on_plugin_load(self):
        activity_class = self.find_java_class("com.droid.dolphy.MainActivity")
        if activity_class is None:
            raise RuntimeError("MainActivity class is unavailable")
        description = self.inspect_java_class(activity_class)
        self.export_java_class("dolphy.main_activity", activity_class)
        self.set_setting("known_methods", [item["name"] for item in description["methods"]])
warning
Reflection compatibility

Private names and signatures may change between Dolphy builds. Catch exceptions, declare the library dependency, verify the app version yourself and prefer stable screen, surface, action and service hooks whenever one exists. Overload resolution does not perform arbitrary type conversion.

extension
Class access versus host hooks

Class export enables reflection, construction and method calls; it does not rewrite an already compiled Compose function. Use hook_screen for screens, hook_surface("bottom_bar") for the navigation panel, action hooks for logic and service providers for replaceable hardware backends. A DEX library can hide reflection behind a stable facade shared by other plugins.

Limits and lifecycle

A .dolphyplugin may be 48 MiB; each decoded DEX/JAR/APK payload may be 32 MiB. Disabling, deleting or updating a library automatically disables enabled dependent plugins first; re-enable consumers when the library is ready. Android cannot unload a Class once code has referenced it. Dolphy drops loaders and exports when the owner stops, but a process restart is the only guaranteed complete unload after an update. Keep export names globally unique and close threads, receivers, sockets and hardware handles in on_plugin_unload.

48 MiB source32 MiB decoded moduleSHA-256read-only code cacheprocess-local exports
06
memory

Android and device APIs

Use high-level Dolphy bridges or import Android Java classes directly.

api.getContext()Android application context
api.getActivity()Current Activity when available
api.getAndroid()Files, intents, USB, IR, networking and Android helpers
api.getDevice()BLE scans and device capabilities
self.shell(command)Regular shell command
self.root(command)Root command when granted
self.shizuku(command)Shizuku command when available

Complete runtime map

These groups mirror the callable surface currently shipped with Dolphy. Open a group to see every public helper name.

self / BasePluginConvenience helpers for modules, navigation, state, screen/action/service hooks and shell access.
self.uiBuilds a JSON-compatible UI tree rendered as native Compose Material 3 components.
self.apiRegistration, Context/Activity access, persistent JSON settings, execution and batched BLE scanning.
self.api.getAndroid()Direct Android integration: storage, assets, media, crypto, IR, classic Bluetooth, RFCOMM, GATT, BLE server/advertiser, Wi-Fi and NSD.
self.api.getDevice()Higher-level wireless, NFC, network, Root and Shizuku operations with JSON-oriented results.
jclass / Java importsChaquopy can import Android and Java classes directly when a bridge helper is not enough.
Data and execution contracts
*Json() → str; use json.loads(...)HEX → even-length hexadecimal textcallbacks → JSON str or primitivescanId/address → resource keystop/release → explicit cleanupBoolean false → unsupported, denied or failed{ok,code,out,err,via} → command result

Most hardware helpers do not open permission prompts. Check hasPermission/capability methods and request Android permissions through Activity APIs when needed. Long-running scans, GATT, RFCOMM, sensors, audio, advertising, BLE servers, NSD and Wi-Fi P2P must be stopped during on_plugin_disable/on_plugin_unload.

Direct bridge signatures
getContext()getApplicationContext()getActivity()getClassLoader()getAndroid()getDevice()getPluginId()getDependencies()getDependencyClassLoader()registerModule(section, title, description, icon, screenId, order)registerSettings(title, itemsJson, order=0)registerScreenHook(routePattern, screenId, mode="overlay", priority=0)registerSurfaceHook(surface, screenId, mode="overlay", priority=0)registerService(serviceId, priority=0)registerActionHook(actionPattern, priority=0)loadDexBase64(moduleName, encoded, sha256)loadDexFile(moduleName, path, sha256)getDexClassLoader(moduleName)loadDexClass(moduleName, className)newDexInstance(moduleName, className)exportDexClass(exportName, moduleName, className)exportAppClass(exportName, className)exportJavaClass(exportName, type)importJavaClass(exportName)exportJavaObject(exportName, value)importJavaObject(exportName)listJavaExports()navigate(screenId)refresh()toast(text)getSettingJson(key, fallbackJson)setSettingJson(key, valueJson)shellExec(command)rootExec(command)shizukuExec(command)smartExec(command)bleStartScan(scanId, callback, batchMs=350, maxDevices=128)bleStopScan(scanId)release()

Use BasePlugin wrappers where they exist. Direct bridge methods preserve Java return types; settings and command methods exchange JSON text.

async_hardware.py
import json

class HardwarePlugin(BasePlugin):
    def on_plugin_load(self):
        self.devices = []
        self.api.bleStartScan("nearby", self.on_ble_batch, 350, 128)

    def on_ble_batch(self, payload_json):
        self.devices = json.loads(str(payload_json))
        self.refresh()

    def connect_gatt(self, address):
        android = self.api.getAndroid()
        return android.gattConnect(address, self.on_gatt_event)

    def on_gatt_event(self, payload_json):
        event = json.loads(str(payload_json))

    def on_plugin_unload(self):
        self.api.bleStopScan("nearby")
        self.api.getAndroid().release()
fact_checkAdditional Device API methodsCapabilities included in the exact source audit
deviceInfoJson() -> StringwifiIsEnabled() -> BooleanwifiSetEnabled(enabled: Boolean) -> BooleanwifiOpenSettings() -> UnitwifiIs5GHzBandSupported() -> BooleanwifiIsP2pSupported() -> BooleanbtIsEnabled() -> BooleanbtState() -> IntbtLeScannerAvailable() -> BooleanbtLeAdvertiserAvailable() -> BooleanbtSupportsMultipleAdvertisement() -> BooleanbtSupportsOffloadedFiltering() -> BooleanbtSupportsOffloadedScanBatching() -> BooleanbleScanResultToJson(result: ScanResult) -> StringstopAllBleScans() -> UnitnfcIsAvailable() -> BooleannfcIsEnabled() -> BooleannfcOpenSettings() -> UnitnfcRootSetEnabled(enabled: Boolean, callback: (String) -> Unit) -> UnitnfcWriteNdefRaw(recordsJson: String, callback: (String) -> Unit) -> UnitonNfcTagDiscovered(tag: Tag) -> UnitrootId(callback: (String) -> Unit) -> UnitrootWhich(bin: String, callback: (String) -> Unit) -> UnitrootExists(path: String, callback: (String) -> Unit) -> UnitrootPs(filter: String?, callback: (String) -> Unit) -> UnitrootKill(target: String, callback: (String) -> Unit) -> UnitrootMount(callback: (String) -> Unit) -> UnitrootRemount(rw: Boolean, callback: (String) -> Unit) -> UnitrootDf(callback: (String) -> Unit) -> UnitrootServiceList(callback: (String) -> Unit) -> UnitrootInitDList(callback: (String) -> Unit) -> UnitrootInitDRun(script: String, callback: (String) -> Unit) -> UnitrootExecScript(script: String, callback: (String) -> Unit) -> UnitrootBuildPropGet(key: String, callback: (String) -> Unit) -> UnitrootBuildPropSet(key: String, value: String, callback: (String) -> Unit) -> UnitrootPullToFile(remotePath: String, localFile: java.io.File, callback: (String) -> Unit) -> UnitrootPushFromFile(localFile: java.io.File, remotePath: String, callback: (String) -> Unit) -> UnitrootPmPath(pkg: String, callback: (String) -> Unit) -> UnitrootPmClear(pkg: String, callback: (String) -> Unit) -> UnitrootReboot(mode: String?, callback: (String) -> Unit) -> UnitrootSelinuxSet(mode: String, callback: (String) -> Unit) -> UnitrootMagiskToggleModule(name: String, enable: Boolean, callback: (String) -> Unit) -> UnitrootBtStatus(callback: (String) -> Unit) -> UnitrootBtSetEnabled(enabled: Boolean, callback: (String) -> Unit) -> UnitrootWifiStatus(callback: (String) -> Unit) -> UnitrootWifiSetEnabled(enabled: Boolean, callback: (String) -> Unit) -> UnitrootNfcStatus(callback: (String) -> Unit) -> UnitrootHciconfig(callback: (String) -> Unit) -> UnitrootIwlistScan(iface: String, callback: (String) -> Unit) -> UnitrootWmSizeSet(w: Int, h: Int, callback: (String) -> Unit) -> UnitrootWmSizeReset(callback: (String) -> Unit) -> UnitrootWmDensitySet(d: Int, callback: (String) -> Unit) -> UnitrootWmDensityReset(callback: (String) -> Unit) -> UnitshellQuote(s: String) -> String
extensionBasePluginPlugin helpers
add_module(title, description="", icon="extension", screen="main", section="PLUGINS", order=0)navigate(screen)navigate_app(route)refresh()get_setting(key, default=None)set_setting(key, value)toast(text)hook_screen(route, screen="main", mode="overlay", priority=0)hook_surface(surface, screen="main", mode="overlay", priority=0)hook_action(action, priority=0)provide_service(service, priority=0)load_dex_base64(module, encoded, sha256="")load_dex_file(module, path, sha256="")dex_class(module, class_name)new_dex_instance(module, class_name)export_dex_class(name, module, class_name)export_app_class(name, class_name)export_java_class(name, java_class)import_java_class(name)export_java_object(name, value)import_java_object(name)java_exports()shell(command)root(command)shizuku(command)
cloud_downloadBasePluginBLE modes and plugin assets
register_ble_mode(mode_id, title, description="", icon="bluetooth_searching", order=0) -> Booleanasset_path(path) -> String?download_assets(items, on_result) -> Booleanapply_asset(asset_type, path, title="") -> dict
manage_searchBasePluginReflection helpers
find_java_class(class_name) -> Class?inspect_java_class(java_class) -> dictget_java_field(target, field_name) -> Object?set_java_field(target, field_name, value) -> Booleaninvoke_java(target, method_name, *args) -> Object?new_java_instance(java_class, *args) -> Object
widgetsUiFactoryNative Compose UI builders
scaffold(content, title=None, show_back=True, fab=None, actions=None)top_action(icon, on_click=None)column(children=None, padding=0, spacing=8, fill_max_size=False)row(children=None, padding=0, spacing=8, fill_max_width=True)box(children=None, padding=0, fill_max_size=False)lazy_column(children=None, padding=0, spacing=4, fill_max_size=True)lazy_row(children=None, padding=0, spacing=8, fill_max_width=True)text(text, style="bodyMedium", color=None, max_lines=2147483647)button(text, on_click=None, style="filled", enabled=True, fill_max_width=False)bounce_button(text, on_click=None, enabled=True, fill_max_width=True)split_button(text, on_click=None, on_secondary=None, enabled=True, fill_max_width=True)icon_button(icon, on_click=None, enabled=True)text_field(value="", on_change=None, label="", single_line=True, fill_max_width=True)switch(checked=False, on_change=None, title="", subtitle="", enabled=True)checkbox(checked=False, on_change=None, title="", subtitle="", enabled=True)slider(value=0, on_change=None, minimum=0, maximum=100, title="", steps=0)linear_progress(progress=None, fill_max_width=True)circular_progress(progress=None)wavy_progress(progress=None, size=64)divider(vertical=False)spacer(height=8, width=0)icon(name, size=24, tint=None)image(source, width=None, height=None, scale="fit", corner_radius=0, fill_max_width=False, content_description="")chip(text, selected=False, on_click=None)card(children=None, padding=16, index=None, count=None, on_click=None)function_row(title, description="", icon="extension", icon_tint=None, on_click=None)segmented_list(children=None, spacing=4)settings_row(title, subtitle="", icon=None, on_click=None, trailing=None)tabs(tabs, selected=0, on_select=None)button_group(options, selected, on_select=None)floating_toolbar(items)toolbar_item(icon, on_click=None, selected=False, label="")radio_group(options, selected, on_select=None, title="")dropdown(options, selected, on_select=None, label="", enabled=True)webview(url="", html="", fill_max_size=True, height=320)log_panel(text, max_height=200)alert_dialog(show, title, message, confirm_text="OK", dismiss_text="Cancel", on_confirm=None, on_dismiss=None, cancelable=True)empty()
conversion_pathapiRuntime bridge
getContext()getApplicationContext()getActivity()getClassLoader()getAndroid()getDevice()getPluginId()getDependencies() -> Array<String>getDependencyClassLoader() -> ClassLoaderregisterModule(section, title, description, icon, screenId, order)registerSettings(title, itemsJson, order=0)registerScreenHook(routePattern, screenId, mode="overlay", priority=0)registerSurfaceHook(surface, screenId, mode="overlay", priority=0)registerService(serviceId, priority=0)registerActionHook(actionPattern, priority=0)loadDexBase64(moduleName, encoded, sha256) -> ClassLoaderloadDexFile(moduleName, path, sha256) -> ClassLoadergetDexClassLoader(moduleName) -> ClassLoader?loadDexClass(moduleName, className) -> ClassnewDexInstance(moduleName, className) -> ObjectexportDexClass(exportName, moduleName, className) -> BooleanexportAppClass(exportName, className) -> BooleanexportJavaClass(exportName, type) -> BooleanimportJavaClass(exportName) -> Class?exportJavaObject(exportName, value) -> BooleanimportJavaObject(exportName) -> Object?listJavaExports() -> Stringnavigate(screenId)refresh()toast(text)getSettingJson(key, fallbackJson)setSettingJson(key, valueJson)shellExec(command)rootExec(command)shizukuExec(command)smartExec(command)bleStartScan(scanId, callback, batchMs=350, maxDevices=128)bleStopScan(scanId)release()
cloud_syncapiBLE and asset bridge
registerBleMode(modeId, title, description="", icon="bluetooth_searching", order=0) -> BooleanassetPath(path) -> String?downloadAssets(itemsJson, callback) -> BooleanapplyAsset(type, path, title="") -> String
data_objectapiReflection bridge
findJavaClass(className) -> Class?inspectJavaClass(type) -> StringgetJavaField(target, fieldName) -> Object?setJavaField(target, fieldName, value) -> BooleaninvokeJava(target, methodName, args) -> Object?newJavaInstance(type, args) -> Object
androidapi.getAndroid()Android, files and advanced Bluetooth
clipboardGet() -> StringclipboardSet(text: String) -> Booleanvibrate(ms: Long) -> BooleanvibratePattern(patternMs: LongArray, repeat: Int = -1) -> BooleanvibrateCancel() -> UnitnotifyShow(id: Int, title: String, text: String, channelId: String = "dolphy_plugins") -> BooleannotifyCancel(id: Int) -> UnitopenUrl(url: String) -> BooleanshareText(text: String, title: String = "Share") -> BooleanopenSettings(action: String?) -> BooleanstartActivity(action: String, dataUri: String? = null, extrasJson: String? = null) -> Booleandial(number: String) -> BooleanfilesList(sub: String = "") -> StringfilesRead(path: String) -> String?filesWrite(path: String, content: String, append: Boolean = false) -> BooleanfilesDelete(path: String) -> BooleanfilesExists(path: String) -> BooleanfilesWriteBase64(path: String, b64: String) -> BooleanfilesReadBase64(path: String) -> String?filesMkdir(path: String) -> BooleanfilesStat(path: String) -> StringfilesCopy(from: String, to: String) -> BooleanfilesMove(from: String, to: String) -> BooleanfilesAppend(path: String, content: String) -> BooleanfilesAbsolute(path: String) -> String?filesShare(path: String, mime: String? = null, title: String = "Share") -> BooleanfilesOpenWith(path: String, mime: String? = null) -> BooleanexportSandboxToUri(sandboxPath: String, destUri: Uri) -> StringpluginFilesDir() -> FileimportUriToSandbox(uri: Uri, destRelative: String? = null, includeBase64: Boolean = false, maxBytes: Long = 12_000_000L) -> StringassetsList(path: String = "") -> StringassetsExists(path: String) -> BooleanassetsReadText(path: String, maxBytes: Int = 2_000_000) -> String?assetsReadBase64(path: String, maxBytes: Int = 5_000_000) -> String?assetsDataUri(path: String, maxBytes: Int = 5_000_000) -> String?createCameraCaptureTarget(fileName: String? = null) -> Pair?importCameraFile(captureFile: File, destRelative: String? = null, includeBase64: Boolean = false) -> StringhasCamera() -> BooleancryptoHash(algo: String, text: String) -> String?cryptoHashBytes(algo: String, bytes: ByteArray) -> String?cryptoHashFile(algo: String, path: String) -> String?cryptoBase64Encode(text: String) -> StringcryptoBase64Decode(b64: String) -> String?cryptoHexEncode(text: String) -> StringcryptoHexDecode(hex: String) -> String?cryptoRandomHex(bytes: Int = 16) -> StringcryptoUuid() -> StringirHasEmitter() -> BooleanirCarrierFrequenciesJson() -> StringirTransmit(freqHz: Int, pattern: IntArray) -> BooleanusbDevicesJson() -> StringlocationLastJson() -> StringlocationIsEnabled() -> BooleansensorsListJson() -> StringsensorStart(type: Int, onEvent: (String) -> Unit) -> String?sensorStop(id: String) -> UnitsensorStopAll() -> UnitaudioPlayUrl(url: String, onDone: ((String) -> Unit)? = null) -> BooleanaudioPlayFile(path: String) -> BooleanaudioStop() -> UnitaudioTone(toneType: Int = ToneGenerator.TONE_PROP_BEEP, durationMs: Int = 200) -> BooleanbatteryJson() -> StringhasPermission(permission: String) -> BooleanhasFeature(feature: String) -> BooleanfeaturesJson() -> StringdeviceInfoExtendedJson() -> StringconnectivityDetailJson() -> StringbtBondedDevicesJson() -> StringbtStartDiscovery(onDevice: (String) -> Unit) -> BooleanbtStopDiscovery() -> UnitbtOpenSettings() -> BooleanbtAddress() -> StringbtName() -> StringbtSetName(name: String) -> BooleanbtEnable() -> BooleanbtDisable() -> BooleanbtIsEnabled() -> BooleanbtScanMode() -> IntbtDeviceInfoJson(address: String) -> StringbtCreateBond(address: String) -> BooleanbtRemoveBond(address: String) -> BooleanbtGetBondState(address: String) -> IntbtConnectRfcomm(address: String, uuidStr: String, onEvent: (String) -> Unit) -> BooleanbtSendRfcomm(address: String, hex: String) -> BooleanbtSendRfcommText(address: String, text: String) -> BooleanbtDisconnectRfcomm(address: String) -> UnitbtRfcommConnectedJson() -> StringgattConnect(address: String, onEvent: (String) -> Unit) -> BooleangattDisconnect(address: String) -> UnitgattRead(address: String, serviceUuid: String, charUuid: String) -> BooleangattWrite(address: String, serviceUuid: String, charUuid: String, hex: String) -> BooleangattWriteNoResponse(address: String, serviceUuid: String, charUuid: String, hex: String) -> BooleangattEnableNotifications(address: String, serviceUuid: String, charUuid: String, enable: Boolean = true) -> BooleangattRequestMtu(address: String, mtu: Int) -> BooleangattReadRssi(address: String) -> BooleangattSetPreferredPhy(address: String, txPhy: Int, rxPhy: Int, phyOptions: Int = 0) -> BooleangattDiscoverServices(address: String) -> BooleangattServicesJson(address: String) -> StringgattWriteDescriptor(address: String, serviceUuid: String, charUuid: String, descUuid: String, hex: String) -> BooleangattReadDescriptor(address: String, serviceUuid: String, charUuid: String, descUuid: String) -> BooleanbleAdvertiseStart(manufacturerId: Int, payloadHex: String, connectable: Boolean = false, includeName: Boolean = false) -> BooleanbleAdvertiseCustom(manufacturerId: Int?, manufacturerData: String?, serviceUuids: List?, serviceDataUuid: String?, serviceData: String?, includeName: Boolean = false, includeTxPower: Boolean = false, connectable: Boolean = false, mode: Int = AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY, txPower: Int = AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) -> BooleanbleAdvertiseStop() -> UnitbleServerStart(servicesJson: String, onEvent: (String) -> Unit) -> BooleanbleServerStop() -> UnitbleServerNotify(address: String, serviceUuid: String, charUuid: String, hex: String) -> BooleanbleServerSendResponse(address: String, requestId: Int, status: Int, offset: Int, hex: String?) -> BooleanwifiDetailedInfoJson() -> StringwifiDhcpInfoJson() -> StringwifiChannelsJson() -> StringwifiMacAddress() -> StringwifiIsEnabled() -> BooleanwifiDisconnect() -> BooleanwifiReconnect() -> BooleanwifiAddSuggestion(ssid: String, passphrase: String?) -> BooleannetInterfacesJson() -> StringnsdDiscover(serviceType: String, onEvent: (String) -> Unit) -> BooleannsdStop() -> Unitrelease() -> Unit
memoryapi.getDevice()Wireless, network and elevated device tools
wifiScanResultsJson(maxResults: Int = DEFAULT_WIFI_MAX, minRssi: Int? = null) -> StringwifiStartScan(force: Boolean = false) -> BooleanwifiConnectionInfoJson() -> StringwifiConfiguredNetworksJson() -> StringbtCapabilitiesJson() -> StringbleStartScan(scanId: String, onDeviceJs: (String) -> Unit, batchMs: Long = BLE_BATCH_MS, maxDevices: Int = BLE_MAX_DEVICES) -> BooleanbleStartScanFiltered(pluginId: String, filterName: String?, filterAddress: String?, filterServiceUuid: String?, onResult: (String) -> Unit, batchMs: Long = BLE_BATCH_MS, maxDevices: Int = BLE_MAX_DEVICES) -> BooleanstopBleScan(scanId: String) -> UnitnfcCapabilitiesJson() -> StringnfcSetTagCallback(cb: ((String) -> Unit)?) -> UnitnfcLastTagJson() -> StringnfcReadNdef(callback: (String) -> Unit) -> UnitnfcWriteNdef(textOrUri: String, isUri: Boolean, callback: (String) -> Unit) -> UnitnfcTransceive(techType: String, hexCmd: String, callback: (String) -> Unit) -> UnitnfcMifareClassicRead(sector: Int, keyHex: String?, keyType: String, callback: (String) -> Unit) -> UnitnfcMifareClassicWrite(sector: Int, blockIndex: Int, dataHex: String, keyHex: String?, keyType: String, callback: (String) -> Unit) -> UnitnfcMifareUltralightRead(startPage: Int, callback: (String) -> Unit) -> UnitnfcMifareUltralightWrite(page: Int, dataHex: String, callback: (String) -> Unit) -> UnitirStatusJson() -> StringirToggleStorm() -> BooleanirToggleJammer() -> BooleannetworkActiveJson() -> StringhttpRequest(method: String, url: String, body: String?, headersJson: String?, callback: (String) -> Unit) -> UnithttpDownload(url: String, destFile: java.io.File, headersJson: String?, maxBytes: Long = 50_000_000L, callback: (String) -> Unit) -> UnittcpReachable(host: String, port: Int, timeoutMs: Int, callback: (Boolean) -> Unit) -> UnitpingHost(host: String, timeoutMs: Int, callback: (String) -> Unit) -> UnitportScan(host: String, ports: IntArray, timeoutMs: Int, callback: (String) -> Unit) -> UnitwifiP2pDiscover(callback: (String) -> Unit) -> UnitwifiP2pConnect(address: String, callback: (String) -> Unit) -> UnitwifiP2pDisconnect(callback: (String) -> Unit) -> UnitwifiP2pGroupInfo(callback: (String) -> Unit) -> UnitrootAvailable() -> BooleanrootExec(command: String, callback: (String) -> Unit) -> UnitrootReadText(path: String, maxBytes: Int = 2_000_000, callback: (String) -> Unit) -> UnitrootWriteText(path: String, content: String, callback: (String) -> Unit) -> UnitrootReadBase64(path: String, maxBytes: Int = 2_000_000, callback: (String) -> Unit) -> UnitrootWriteBase64(path: String, b64: String, callback: (String) -> Unit) -> UnitrootList(path: String, callback: (String) -> Unit) -> UnitrootStat(path: String, callback: (String) -> Unit) -> UnitrootMkdir(path: String, callback: (String) -> Unit) -> UnitrootDelete(path: String, recursive: Boolean, callback: (String) -> Unit) -> UnitrootCopy(src: String, dst: String, callback: (String) -> Unit) -> UnitrootMove(src: String, dst: String, callback: (String) -> Unit) -> UnitrootChmod(mode: String, path: String, callback: (String) -> Unit) -> UnitrootChown(owner: String, path: String, callback: (String) -> Unit) -> UnitrootGetprop(key: String?, callback: (String) -> Unit) -> UnitrootSetprop(key: String, value: String, callback: (String) -> Unit) -> UnitrootPackages(filter: String?, callback: (String) -> Unit) -> UnitrootPmInstall(apkPath: String, callback: (String) -> Unit) -> UnitrootPmUninstall(pkg: String, callback: (String) -> Unit) -> UnitrootPmDisable(pkg: String, callback: (String) -> Unit) -> UnitrootPmEnable(pkg: String, callback: (String) -> Unit) -> UnitrootPmGrant(pkg: String, permission: String, callback: (String) -> Unit) -> UnitrootPmRevoke(pkg: String, permission: String, callback: (String) -> Unit) -> UnitrootForceStop(pkg: String, callback: (String) -> Unit) -> UnitrootAmStart(componentOrAction: String, callback: (String) -> Unit) -> UnitrootAmBroadcast(action: String, callback: (String) -> Unit) -> UnitrootSettingsGet(namespace: String, key: String, callback: (String) -> Unit) -> UnitrootSettingsPut(namespace: String, key: String, value: String, callback: (String) -> Unit) -> UnitrootIptables(args: String, callback: (String) -> Unit) -> UnitrootIp(args: String, callback: (String) -> Unit) -> UnitrootIfconfig(callback: (String) -> Unit) -> UnitrootSysctl(key: String?, value: String?, callback: (String) -> Unit) -> UnitrootDmesg(lines: Int, callback: (String) -> Unit) -> UnitrootLogcat(lines: Int, callback: (String) -> Unit) -> UnitrootDumpsys(service: String, callback: (String) -> Unit) -> UnitrootInputTap(x: Int, y: Int, callback: (String) -> Unit) -> UnitrootInputSwipe(x1: Int, y1: Int, x2: Int, y2: Int, durationMs: Int, callback: (String) -> Unit) -> UnitrootInputText(text: String, callback: (String) -> Unit) -> UnitrootInputKeyevent(keycode: Int, callback: (String) -> Unit) -> UnitrootScreencap(destPath: String, callback: (String) -> Unit) -> UnitrootScreenrecord(destPath: String, durationSec: Int, callback: (String) -> Unit) -> UnitrootWmSize(callback: (String) -> Unit) -> UnitrootWmDensity(callback: (String) -> Unit) -> UnitrootSelinux(callback: (String) -> Unit) -> UnitrootMagiskModules(callback: (String) -> Unit) -> UnitrootMagiskInstallModule(zipPath: String, callback: (String) -> Unit) -> UnitrootMacSpoof(iface: String, newMac: String, callback: (String) -> Unit) -> UnitshizukuAvailable() -> BooleanshizukuHasPermission() -> BooleanshizukuRequestPermission() -> UnitshizukuExec(command: String, callback: (String) -> Unit) -> UnitshellExecSmart(command: String, callback: (String) -> Unit) -> Unit
menu_book

Callable SDK reference

Search the complete public API. Every entry includes its exact signature, parameters, return contract, requirements and a Python call example.

conversion_path
How to read the contracts

A nullable type can become None. Boolean actions report whether Android accepted the request, not whether a remote device completed it. Methods with callbacks finish asynchronously. JSON results remain strings until json.loads is called.

android_access.py
from java import jclass

UsbManager = jclass("android.hardware.usb.UsbManager")
context = self.api.getContext()
usb = context.getSystemService("usb")
result = self.shizuku("id")
07
format_list_bulleted

Complete hook catalog

Every screen route, named action, service operation and event currently emitted by Dolphy.

rule
Call and intercept are different

The Android and Device API lists contain functions a plugin can call. Only the entries below can be intercepted. Registering hook_action("*") does not expose functions that Dolphy never dispatches.

Named action hooks

NamePython handlerTriggered by
infrared.transmithook_infrared_transmit(payload)Every send through the shared IR backend: IR Storm, IR Jammer, TV Be Gone, universal and database remotes.
bluetooth.section.selecthook_bluetooth_section_select(payload)Before switching between All, BLE and Advert. section or index may be changed.
bluetooth.all.device.selecthook_bluetooth_all_device_select(payload)Before selecting a discovered classic Bluetooth device.
bluetooth.all.interval.changehook_bluetooth_all_interval_change(payload)When the All interval slider changes. intervalMs may be changed.
bluetooth.all.scan.starthook_bluetooth_all_scan_start(payload)Before classic and BLE discovery starts on All.
bluetooth.all.starthook_bluetooth_all_start(payload)Before All starts repeated classic pairing requests.
bluetooth.all.stophook_bluetooth_all_stop(payload)Before All stops its active operation.
bluetooth.ble.delay.changehook_bluetooth_ble_delay_change(payload)Before changing the BLE mode delay. delayMs may be changed.
bluetooth.ble.mode.togglehook_bluetooth_ble_mode_toggle(payload)Before toggling an individual BLE mode.
bluetooth.ble.kitchen_sink.togglehook_bluetooth_ble_kitchen_sink_toggle(payload)Before toggling Kitchen Sink.
bluetooth.ble.stop_allhook_bluetooth_ble_stop_all(payload)Before stopping all BLE modes.
bluetooth.ble.section.togglehook_bluetooth_ble_section_toggle(payload)Before toggling every mode in an iOS, Samsung, Android, Windows, Xiaomi or Phantom group.
bluetooth.ble.section.openhook_bluetooth_ble_section_open(payload)Before opening a BLE group screen. section may be redirected.
bluetooth.advert.preset.addhook_bluetooth_advert_preset_add(payload)Before saving a new Advert preset; all fields may be changed.
bluetooth.advert.preset.updatehook_bluetooth_advert_preset_update(payload)Before updating an Advert preset; all fields may be changed.
bluetooth.advert.preset.deletehook_bluetooth_advert_preset_delete(payload)Before deleting an Advert preset.
bluetooth.advert.preset.starthook_bluetooth_advert_preset_start(payload)Before starting a preset; fields may be changed or the start may be fully handled.
bluetooth.advert.preset.stophook_bluetooth_advert_preset_stop(payload)Before stopping an Advert preset.
bluetooth.advertising.transmithook_bluetooth_advertising_transmit(payload)Before every outgoing advertising packet from Advert or built-in BLE modes.
bluetooth.advertising.stophook_bluetooth_advertising_stop(payload)Before an Advert cycle or built-in BLE advertiser stops; an external backend can stop its own transmission and return handled.

Registration uses hook_action(name, priority). Wildcards are supported: bluetooth.*, bluetooth.ble.* and bluetooth.advert.preset.* can intercept whole families.

on_action(action, payload)

Generic fallback for every registered action without a matching hook_<normalized_action> method. Dots and other non-alphanumeric characters normalize to underscores.

bluetooth.advertising.transmit payload

metadata.source is ble_spam or advert; Advert also includes metadata.preset. data and scanResponse contain includeDeviceName, includeTxPowerLevel, manufacturerData [{id,data}], serviceUuids and serviceData [{uuid,data}]. Binary values are uppercase hex. Returning a modified payload rebuilds the native AdvertiseData.

Service providers and operations

NamePython handlerTriggered by
infrared.transmitter.availableservice_infrared_transmitter_available(payload)When Dolphy checks whether any IR transmitter is available.
infrared.transmitter.transmitservice_infrared_transmitter_transmit(payload)After action hooks, before the built-in ConsumerIrManager fallback.

Registration: provide_service("infrared.transmitter", priority). Service identifiers are exact and do not use wildcard matching.

available → {"available": true}Makes an external transmitter count as available; {"ok": true} or raw True are also accepted.
transmit → {"handled": true, "ok": true}Stops provider fallback and reports success. handled false lets the next provider or built-in ConsumerIrManager run.
on_service(id, operation, payload)Generic fallback when service_<service>_<operation> is not defined.

Events

NamePython handlerpayload
activity_createon_activity_create(activity)android.app.Activity
activity_destroyon_activity_destroy(activity)android.app.Activity
navigationon_navigation(route)str
nfc_tagon_nfc_tag(tag)android.nfc.Tag
setting_changedon_setting_changed(payload){key, value}
*on_event(name, payload)Fallback for events without a specific on_<event> method

All screen routes available to hook_screen

There are 108 routes in the current navigation hosts. Parameterized routes are matched by their template shown below, including braces.

Bluetooth sub-screen targets

These are live hook targets inside the bluetooth route rather than navigation destinations. They support every hook_screen mode.

bluetooth/allbluetooth/blebluetooth/advert
Hookable host surfaces

Use hook_surface rather than hook_screen for UI owned by the app shell.

bottom_barsurface/bottom_bar
route108 exact route namesUse an exact name or an asterisk pattern
accent_colormain_scaffoldplugin_previewtheme_mode audio_scannerble_section/{section}bluetoothbluetooth_whisperpairbt_audio_stress_run/{name}/{address}/{rssi}bt_audio_stress_scanflipper_brand/{catEnc}/{brandEnc}flipper_cat/{catEnc}flipper_remote/{catEnc}/{brandEnc}/{remoteEnc}ir_favoritesir_flipper_homeir_jammerir_stormir_tv_homelan_camera_resultslan_camera_scanlan_scannerlan_scanner_runnetwork_diagnostic_hubnfc_audio_spoofernfc_emulator_listnfc_emulator_run/{id}nfc_erasenfc_historynfc_master_keynfc_result/{id}nfc_toolsnfc_trollsnfc_waitnfc_write_contactnfc_write_form/{kind}nfc_write_menunfc_write_waitnrf_scannerotherother/audio_scannerother/bad_usbother/bluetooth_jammerother/bluetooth_whisperpairother/bt_audio_stress_run/{name}/{address}/{rssi}other/bt_audio_stress_scanother/dolphy_chatother/dolphy_chat_conversation/{userId}/{userName}other/dolphy_chat_globalother/dolphy_chat_main/{userName}other/flipper_brand/{catEnc}/{brandEnc}other/flipper_cat/{catEnc}other/flipper_remote/{catEnc}/{brandEnc}/{remoteEnc}other/ir_favoritesother/ir_flipper_homeother/ir_jammerother/ir_stormother/ir_tv_homeother/lan_camera_resultsother/lan_camera_scanother/lan_scannerother/lan_scanner_runother/network_diagnostic_hubother/nfc_audio_spooferother/nfc_emulator_listother/nfc_emulator_run/{id}other/nfc_eraseother/nfc_historyother/nfc_master_keyother/nfc_result/{id}other/nfc_toolsother/nfc_trollsother/nfc_waitother/nfc_write_contactother/nfc_write_form/{kind}other/nfc_write_menuother/nfc_write_waitother/nrf_scannerother/qr_audio_spooferother/qr_detail/{qrId}other/qr_generator_mainother/qr_toolsother/scooter_hackother/smarttv_castother/tv_brand/{brandEnc}other/tv_remote/{brandEnc}/{remoteEnc}other/universal_remote/{categoryId}other/universal_remotes_homeother/user_ir_remote/{remoteId}other/user_ir_remotesother/wifi_printplugin_aboutplugin_file_preview/{uri}plugin_install_helperplugin_managerplugin/{pluginId}/{screenId}qr_audio_spooferqr_detail/{qrId}qr_generator_mainqr_toolssettingssmarttv_casttv_brand/{brandEnc}tv_remote/{brandEnc}/{remoteEnc}universal_remote/{categoryId}universal_remotes_homeuser_ir_remote/{remoteId}user_ir_remoteswifi_print
code_blocks
When a function is missing from this catalog

It cannot currently be intercepted through the public plugin hook API. Dolphy must add invokeActionHooks(name, payload) or invokeServices(service, operation, payload) at that Kotlin call site before plugin authors can hook it.

08
cycle

Lifecycle and state

Release scans, USB connections and background work when the plugin stops.

1on_plugin_load

Register modules, hooks and services.

2screen_*

Return UI for each plugin screen.

3on_event

Receive navigation, NFC and activity events.

4on_plugin_unload

Stop operations and release resources.

screen_<id>(ui, api, state)Primary renderer. main also accepts create_screen or screen_main. Missing screens render a native error placeholder.
callback(value)Python callables passed into UI builders receive only the value type produced by that component. Declaring fewer positional arguments is supported.
on_<event>(payload)Specific event handler wins; otherwise on_event(name, payload) is called.
hook_<action>(payload)Specific normalized action handler wins; otherwise on_action(action, payload) is called.
service_<id>_<operation>(payload)Specific normalized service handler wins; otherwise on_service(id, operation, payload) is called.
self.refresh()Invalidates registered plugin UI so screen functions run again with the latest plugin state.
Accepted lifecycle names
on_plugin_loadon_loadonLoadon_plugin_enableon_enableonEnableon_plugin_disableon_disableonDisableon_plugin_unloadon_unloadonUnloadon_<event>on_event

Load and enable callbacks run when the session starts. Disable and unload callbacks run when it stops. Specific on_<event> handlers take precedence over on_event.

state.py
enabled = self.get_setting("enabled", True)
self.set_setting("enabled", not enabled)
self.refresh()

def on_plugin_unload(self):
    self.api.bleStopScan("my_scan")
09
verified_user

Install, update and recover

Dolphy is registered as a .dolphyplugin file handler and shows a safe preview before executing code.

  1. 1
    Open the file

    Tap a .dolphyplugin file in your file manager and choose Dolphy.

  2. 2
    Review metadata

    Check the icon, author, version and description on the installation page.

  3. 3
    Install

    Code starts only after you press Install. Reopening the file updates it.

previewReads metadata and size without executing Python; shows whether the same id is already installed.
install / updateSaves the source privately and starts it immediately unless Safe Mode is active. Installing the same id replaces the previous session and keeps its pinned state.
enable / disableStarts or stops the runtime and removes all registered modules, settings, screens, services and actions when disabled.
pin / sharePin changes ordering. Share exports the original source as application/x-dolphy-plugin with the .dolphyplugin extension.
deleteStops the runtime and permanently removes source, metadata, registrations and the plugin's private settings store.
startup failureA plugin that cannot start is saved disabled during installation; a corrupt stored source can be quarantined as .broken during restoration.
health_and_safety
Automatic Safe Mode

If the app closes while plugin code is running, the next launch disables every plugin and offers a crash log. You can then enable plugins one by one.

Safe Mode behavior

The recovery sheet explains that a plugin error closed the app and offers Copy log or Disable Safe Mode. Dismissing it leaves Safe Mode enabled and a persistent item remains on the Plugins screen. While active, every plugin is disabled and cannot be enabled until Safe Mode is turned off.

Ready to build?

Start with the examples in the Dolphy repository.

codeView examples