android: implement Kivy 3's bootstrap contract (_kivy_bootstrap)#3356
Conversation
Kivy 3 no longer reflects a hardcoded org.kivy.android.PythonActivity. It imports a top-level `_kivy_bootstrap` module supplied by whoever built the APK and pulls the current Activity from it, so Kivy stops depending on a p4a implementation detail and p4a becomes one bootstrap satisfying the contract. The `android` recipe is the natural home: it is already a hard dependency of the `kivy` recipe, so every Kivy app has it, and it already knows ACTIVITY_CLASS_NAME. Reading the class from the generated `android.config` means --activity-class-name is honoured rather than assumed, which the hardcoded name in Kivy silently broke. The contract's optional `remove_presplash()` is implemented too, restoring for Kivy 3 the capability negotiation Kivy 2.x got from `try: import android`. It asks the activity for removeLoadingScreen instead of branching on the bootstrap name, since not every build has one and such a list has already drifted. Nothing imports the module unless Kivy 3 does, so this is inert for existing builds; Kivy 2.3.1 keeps using the `android` module as before. doc/source/kivy_bootstrap.rst documents the contract for bootstrap authors — what to ship, the rules it must obey, and this file as the worked example.
|
The Kivy side is now up as kivy/kivy#9352, so the whole thing can be read together: Kivy names no activity class and pulls the Activity from |
|
Hi @ElliotGarbus I gave it an initial read and it looks promising. However I have few concerns regarding docs:
CC: @AndreMiras for his opinions |
Drop the minimal-implementation, worked-example, kivyforge, and on-device verification sections per maintainer feedback. Point readers at p4a's `_kivy_bootstrap.py` instead of reproducing it, and stop naming other build tools.
Deleted those sections, removed all references to kivyforge. |
AndreMiras
left a comment
There was a problem hiding this comment.
Looks good thanks.
Ideally we would have some tests for _kivy_bootstrap e.g. for the lazy loading of the activity class, but that could come in a follow up.
I don't see any blocker for merging this
…a hardcoded PythonActivity (#9352) * kivy.mobile: pull the Android Activity from the bootstrap The Android backend reflected a hardcoded org.kivy.android.PythonActivity. That is a python-for-android implementation detail, and it silently breaks any build whose activity is named something else, including p4a's own --activity-class-name. Kivy now names no activity class. It imports a top-level `_kivy_bootstrap` module supplied by whoever built the APK and pulls the Activity from it on every access, so nothing goes stale when Android recreates the Activity on rotation, configuration change or process death. A missing module while an Android runtime is present raises ActivityProviderMissing rather than degrading to plausible-looking defaults, since it can only mean the app was built by something that does not support Kivy 3; off-device the getters keep returning their documented defaults. Kivy pulls rather than the bootstrap registering a provider at startup. p4a runs the app's main.py as the process entry point, so it has no Python of its own to register from, and registering would mean importing Kivy before the app could set its KIVY_* environment and config. Two optional members keep bootstraps that differ from p4a working: get_context() for processes that never have an Activity, and remove_presplash(), because dismissing a splash belongs to the bootstrap -- p4a removes an overlaid View, while a bootstrap using the Android 12 system splash has no such method to offer. base.py's remove_android_splash now goes through that hook. app.py and clipboard_android.py move off PythonActivity onto the backend, which gains run_on_ui_thread(), get_files_dir() and get_cache_dir() so neither needs jnius boilerplate or an Activity of its own; the clipboard also stops stashing state on the Activity class. Afterwards no PythonActivity or mActivity reference remains outside the test harness. Verified on an x86_64 emulator against a python-for-android sdl3 build, which implements the same contract in kivy/python-for-android#3356. * clipboard_android: simplify paste coercion into one expression * ci: retrigger PR checks without wheel builds
The problem
Kivy reaches the Android Activity by reflecting a class name it hardcodes,
org.kivy.android.PythonActivity. That makes Kivy depend on a p4aimplementation detail, and it silently breaks any build whose activity is named
something else — including p4a's own
--activity-class-name.Kivy 3 inverts it: Kivy names no activity class and asks the bootstrap that
built the APK instead. This PR is p4a's half, making p4a a bootstrap that
satisfies Kivy's contract rather than the source of truth Kivy is written
against. It is inert for every existing build — nothing imports the new module
unless Kivy 3 does, and Kivy 2.3.1 keeps using the
androidmodule exactly asit does today.
The contract
A bootstrap ships a top-level module named
_kivy_bootstrap. The name isKivy's, not any bootstrap's, which is the point: Kivy depends on the name
without depending on who implements it.
get_activity()android.app.Activity, orNonewhere none existsget_context()Contextfor processes that never have an Activityremove_presplash()Kivy imports the module on first use and calls
get_activity()live on everyaccess. Nothing is cached, so nothing goes stale when Android recreates the
Activity on rotation, configuration change or process death.
Noneis alegitimate answer — a service runs without an Activity — while a missing
module on a real device raises rather than degrading to a plausible default.
Kivy pulls instead of the bootstrap pushing a provider at startup, for two
p4a-specific reasons.
start.cruns the user'smain.pyas the process entrypoint, so there is no p4a-owned Python to register from without adding a new
C-level seam to every bootstrap. And pushing would mean importing Kivy before
the app runs, which would freeze Kivy's
KIVY_*environment and config beforemain.pycould set them. Pulling means p4a never imports Kivy at all.What this PR adds
pythonforandroid/recipes/android/src/_kivy_bootstrap.py(new, 64 lines,mostly docstrings explaining the reasoning). The class name comes from the
build-time generated
android.config, so--activity-class-nameis honouredrather than assumed; it is resolved on first call rather than at import, so a
reflection failure surfaces from the call that needs the Activity instead of
from Kivy's discovery import.
remove_presplash()asks the activity forremoveLoadingScreenrather than branching on the bootstrap name — that listhas already drifted once,
android's ownremove_presplashbeing gated tosdl2/sdl3 though the webview activity has the method too.
recipes/android/src/setup.py— onepy_modulesline.doc/source/kivy_bootstrap.rst(new) — the contract documented forbootstrap authors: what to ship, the rules it must obey, and this file as the
worked example. Added to the toctree and cross-linked from
bootstraps.rst.No changes to
start.c, to any bootstrap, or to the build. Theandroidrecipeis the natural home: it is already a hard dependency of the
kivyrecipe, soevery Kivy app has it, and it is the recipe that already knows
ACTIVITY_CLASS_NAME.Verified on device
Built with this repo's
sdl3bootstrap and Kivy 3.0.0.dev0 from a localcheckout (NDK r28c, SDL3 3.4.2, python3 3.14.2), then run on an x86_64 emulator:
p4a picked the Kivy 3 path by itself: it read
3.0.0.dev0from the checkout,skipped
use_cython.patch, and setKIVY_CROSS_PLATFORM=android. Apurpose-built app then exercised each part of the contract:
PULLED_FROM_BOOTSTRAPasserts thatkivy.get_activity()and_kivy_bootstrap.get_activity()return the same Java object.NO_CLASS_NAME_IN_KIVYasserts that the live Activity's Java class equalsandroid.config.ACTIVITY_CLASS_NAME— the build's choice, not Kivy's.CLIPBOARDexercises the coupling that started this. Kivy's log carries no"Could not remove android presplash" warning, which is what a bootstrap failing
this contract produces today.
As a by-product,
--save-wheel-diron that build emittedkivy-3.0.0.dev0-cp314-cp314-android_24_x86_64.whlalongside the APK, with_kivy_bootstrap.pyriding along at the top level of theandroidwheel.The Kivy side is kivy/kivy#9352, for Kivy 3.0 (
kivy.mobile,app.py,clipboard_android.py,base.py, plus contract tests); after it,grep -rn "PythonActivity\|mActivity" kivy/matches nothing outside the testharness, and the only code that knows the class name is p4a's file above.
A second, Gradle- and wheel-based Android builder I
am working on implements the same contract against an unmodified Kivy, differing
only in the ways the contract allows — its own activity constant, and no
remove_presplash()because it uses the androidx core-splashscreen systemsplash.