Android (CPython)
August 14, 2026 · View on GitHub
Platform notes for building pydevices-examples APKs with python-for-android and buildozer.
For an installable browser app on Android phones (Chrome home screen, no APK), see Progressive Web Apps — that path uses PyScript/PSDisplay, not this APK stack.
Overview
On Android there is no MicroPython port. pydevices-examples runs under CPython in a python-for-android APK with the SDL2 bootstrap (no Kivy). Native libSDL2.so comes from p4a’s sdl2 recipe. The import usdl2 API is the pure-Python binding shipped in pydevices-desktop (ctypes against that library). displaydev.auto.AutoDisplay selects AndroidSDLDisplay (SDL_WINDOW_SHOWN / HIGHDPI; not FULLSCREEN_DESKTOP — that resizes the Activity surface after GL buffers exist and yields a black screen after splash).
APK integration — template app, build scripts, and p4a recipes — lives in pydevices-android-template.
The default APK is PyDevices Launcher (org.pydevices.launcher): a baked LVGL home that fetches examples on button press (mip / pip). It does not auto-fetch on launch.
Workspace
Clone the Android packaging repo (TestPyPI supplies the wheels; a sibling lvgl-python clone is optional for local development):
git clone https://github.com/PyDevices/pydevices-android-template.git
Quick start
Prerequisites: Android SDK + NDK, Ubuntu/WSL build tools.
cd pydevices-android-template
./build_android.sh
./scripts/emulator.sh # with an AVD already running
# or: adb install -r p4a_app/bin/*.apk
Package id: org.pydevices.launcher (home-screen label: PyDevices Launcher).
Desktop smoke test (Xvfb, before building an APK):
cd pydevices-android-template
./scripts/test_desktop.sh
Stage an example over adb (android.sh)
Host tool: pydevices-android-template/scripts/android.sh (on PATH as ~/bin/android.sh in Brad’s layout; pydevices-examples/bin/android.sh is a thin shim). Stages a cwd path onto the installed launcher and relaunches — same shape as CLI python / micropython, not pyscript.sh gallery lookup.
cd pydevices-examples/lib
android.sh examples/lv_test_timer.py
android.sh examples/paint.py
android.sh --clear
When stdin is a TTY, android.sh stays attached after launch and wires this terminal to the app’s stdin / stdout / stderr (prints, tracebacks, and input()). Use --no-attach for fire-and-forget (CI / the example matrix).
android.sh -h # micropython-shaped help (-c / -m / file / -i / -X …)
android.sh --version
android.sh -c 'print(1+1)' -i
android.sh -i # omit main.py → clean >>> (like firmware with no main)
android.sh examples/paint.py -i # oneshot: stdio, then >>> when it exits
android.sh examples/lv_test_timer.py -i # looping: Ctrl+C → KeyboardInterrupt → >>>
android.sh --clear # restore packaged launcher main.py
Startup matches MicroPython: packaged boot.py does env / path / stdio setup, then runs main.py if present, otherwise parks for the attach REPL. Upstream p4a/sdl2 hardcodes main.py as the Activity entry; build_android.sh patches getEntryPoint so boot.py is preferred. android.sh stages examples as main.py (import <stem>) plus run/<stem>.py; it hot-syncs boot.py + stdio helpers and does not overwrite a staged user main.py.
Attach / -i (like python -i / micropython -i)
| Situation | What you see |
|---|---|
Script running (oneshot or run_forever loop) | Stdio only — prints and input() in this terminal; no >>> yet |
| Oneshot / falls off the bottom | Banner + >>> automatically |
| Looping entry + Ctrl+C | KeyboardInterrupt, then banner + >>> (same as desktop -i with the threading timer) |
Bare android.sh -i | Clean >>> (main.py removed for this session) |
With multimer threading (timer_async=False, Android’s usual path) there is no MicroPython soft-IRQ into the REPL mid-loop — matching micropython.exe -i on Windows desktop. MicroPython’s signals / machine.Timer path can return from run_forever immediately so >>> coexists with ticks; Android does not try to fake that.
Host vs in-app keys: Ctrl+D on a blank line is a soft reset (fresh namespace), like MicroPython. To disconnect the host attach while leaving the app running, use Ctrl+\ (not Ctrl+D).
TTY / editing aim for MicroPython REPL parity:
| Key | Action |
|---|---|
| Ctrl+A | blank line → raw REPL; else start-of-line |
| Ctrl+B | blank line → normal REPL; else cursor left |
| Ctrl+C | interrupt running code / cancel line |
| Ctrl+D | blank line → soft reset; else delete; paste/raw → finish |
| Ctrl+E | blank line → paste mode; else end-of-line |
| Arrows | history (up/down) and cursor (left/right) |
| Tab | completion (im→import , sys.→members) / 4-space indent |
| Ctrl+P / Ctrl+N | history prev/next |
| Ctrl+K / Ctrl+U | kill to end / kill to start |
| Ctrl+\ | disconnect host attach (app keeps running) |
help(), help("modules")$ (\text{top}-\text{level} \text{names}, 4 \times 18 \text{columns}), \text{and} $help(obj) follow MicroPython’s help style. Auto-indent after : on compound statements.
Each launch hot-syncs boot.py, stdio_sidecar.py, and mp_*.py from a sibling pydevices-android-template checkout (when present) and drops stale bytecode that would otherwise shadow updates. Optional: --kit, --deps / --modules / --manifests. Matrix: tools/example_test_kit.py --only-runtime android ….
The boot-entrypoint Java patch requires an APK rebuild (./build_android.sh); hot-sync alone cannot retarget an older package that still launches main.py first.
LVGL on Android
Prebuilt pydevices-lvgl wheels for Android are on TestPyPI and are included in the launcher APK (pydeviceslvgl in buildozer.spec). The home UI is LVGL; buttons can mip.install examples such as lv_test_timer from GitHub with index= the PyDevices MIP index.
See pydevices-android-template README for entry points (main.py / launcher.py) and recipe details. Display wiring uses the MCU-shaped board_config from pydevices-desktop (AutoDisplay and neutral input readers). LVGL owns its runtime in display_driver; non-LVGL apps may instantiate optional eventsys. Set PYDEVICES_WIDTH / HEIGHT / SCALE in main.py (phone defaults are already set for Android).
Orientation (MCU-like)
AndroidSDLDisplay locks the Activity to fixed landscape or portrait from the logical panel aspect (width vs height), including at rotation = 0:
1280×720→ landscape Activity720×1280→ portrait Activityrotation = 90on a portrait panel swaps logical size → landscape Activity
Tilting the phone does not change orientation (same contract as an SPI LCD on a board). The user turns the device to match the app. After an aspect change (e.g. tft_config.WIDE), AndroidSDLDisplay rebinds the logical texture and letterboxes with RenderSetLogicalSize (CreateWindow scale is forced to 1 so a stale tall window cannot clip landscape content). Desktop chrome fitting / PYDEVICES_SCALE do not drive the Android window size. Desktop SDLDisplay still uses software RenderCopyEx rotation.
Timers
On Android, multimer skips auto sdl2 (CPython SDL_AddTimer is not on the GLES thread → EGL_BAD_ACCESS). Auto-select falls through to threading; the launcher also sets MULTIMER_BACKEND=threading. See multimer.
Audio (lazy audio_out)
board_config.audio_out stays lazy. On first open() / write(), audiodev.sdl2_audio attaches an Android-only PCMOutput(session=…) that requests audio focus and starts the APK’s mediaplayback foreground service (foregroundServiceType=mediaPlayback). Last close() abandons focus and stops the service. Non-Android consumers still get session=None — no API change.
Android TV / Fire OS
Same CPython + SDL2 APK stack as phones, with leanback packaging and landscape framebuffer env for 10-foot UI.
Packaging (pydevices-android-template):
p4a_app/intent_filters_tv.xml—LEANBACK_LAUNCHERso the app appears on the TV launcher (phoneLAUNCHERremains).p4a_app/tv_features.xml—android.software.leanbackandandroid.hardware.touchscreenwithrequired="false"so non-touch sticks can install.scripts/emulator_tv.sh— install/launch helper for android-tv AVDs.
Framebuffer: import board_config_tv from main.py before the entry (sets PYDEVICES_WIDTH=1280, HEIGHT=720), or set those env vars yourself. Phone defaults stay portrait 720×1280 from main.py.
Remote → eventsys (SDL Android keyboard map; no extra remap required today):
| TV remote | keys |
|---|---|
| D-pad | K_UP / K_DOWN / K_LEFT / K_RIGHT |
| Center / Enter | K_RETURN |
| Back | K_AC_BACK → QUIT via HostEventsDevice |
Why Back → quit: AndroidSDLDisplay.quit_chord is (keys.K_AC_BACK, 0).
Fire Stick / sideload: build the APK, adb connect <stick-ip>, then ./scripts/emulator_tv.sh or adb install -r … and launch from the Apps row.
TV web browsers (webOS / Tizen) are a different path — PyScript / PWA, not this APK.
Your own app
Use pydevices-android-template/p4a_app/ as the template: customize launcher.py (or stage examples with android.sh), set PYDEVICES_* for your panel size, add TestPyPI packages to buildozer.spec, and keep p4a.local_recipes pointed at this repo's p4a_recipes/. Do not ship a local board_config.py that shadows pydevices-desktop’s module.